编译期注解学习六- 生成java文件javapoet

1 生成文件方式

编译期注解通过读取注解,然后处理注解字段Element,最终的目的是在编译代码之前生成所需源码文件作为工具类,减少手动书写代码。
生成文件的方式有多种,例如StringBuilder进行拼接,模板文件进行字段替换,javaPoet 生成。
模板生成代码
StringBuilder进行拼接,模板文件进行字段替换进行简单文件生成还好,如果是复杂文件,拼接起来会相当复杂如下所示:

  private String brewCode(String className, String pkName, ArrayList<VariableElement> mElementList) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkName + ";\n\n");
        builder.append("public class " + className + "$ViewBinding implements com.ldx.injectlib.InjectIoc { \n\n");
        builder.append("@Override\n\n");
        builder.append("public void inject("+"Object"+" obj1"+"){ \n\n");
        builder.append("        "+pkName+"."+className+" obj" +" = "+"("+pkName+"."+className+")"+"obj1;\n\n");
        for (VariableElement element : mElementList){
            //3.获取注解的成员变量名
            String bindViewFiledName = element.getSimpleName().toString();
            //变量类型
            String bindViewFiledClassType = element.asType().toString();
            BindView bindAnnotation = element.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            String info = String.format("%s %s = findViewById(%d)", bindViewFiledClassType, bindViewFiledName, id);
            builder.append("        System.out.println(\"" + info + "\");\n\n");
            builder.append("        obj."+bindViewFiledName+" = "+"obj.findViewById("+id+");"+"\n\n");
        }

        builder.append("}\n\n");
        builder.append("}");
        return builder.toString();
    }

javaPoet生成代码是Square出品的sdk,学习成本较小,用起来相当顺手。
简单代码示例

private void createClass(){
        //https://github.com/square/javapoet
        try {
            MethodSpec methodSpec = MethodSpec.methodBuilder("main")
                    .addModifiers( javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(String[].class,"args")
                    .addStatement("$T.out.println($S)",System.class,"hello world!")
                    .build();
            TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                    .addModifiers( javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.FINAL)
                    .addMethod(methodSpec)
                    .build();

            JavaFile javaFile = JavaFile.builder("com.ldx.canvasdrawdemo", typeSpec)
                    .build();

            mMessager.printMessage(Diagnostic.Kind.NOTE, javaFile.toString()+"");
            javaFile.writeTo(mFilerUtils);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

最终生成的文件:

package com.ldx.canvasdrawdemo;
 
  import java.lang.String;
  import java.lang.System;
  
  public final class HelloWorld {
    public static void main(String[] args) {
      System.out.println("hello world!");
    }
  }

2 javapoet 使用讲解

需要引入javapoet的库 implementation 'com.squareup:javapoet:1.11.1';

本篇大致讲解,详细内容参照(https://github.com/square/javapoet),一般生成代码的方式为从Class开始,缺少什么在前面生成对应的代码模块,add到classtype中最终完成class的生成。

常用到的api:

用到的类(多为建造者模式,链式调用):

  • TypeSpec:用于生成类、接口、枚举对象的类(class interface enum)
  • MethodSpec:用于生成方法对象的类(生成method)
  • ParameterSpec:用于生成参数对象的类(方法的参数)
  • AnnotationSpec:用于生成注解对象的类(生成注解)
  • FieldSpec:用于配置生成成员变量的类(成员变量)
  • ClassName:通过包名和类名生成的对象,指明为某个Class,功能类似xxx.class
  • ParameterizedTypeName:通过MainClass和IncludeClass生成包含泛型的Class

用到的方法:

创建类:
TypeSpec.classBuilder("类名“)
TypeSpec.classBuilder(ClassName className)

创建接口:
TypeSpec.interfaceBuilder("接口名称")
TypeSpec.interfaceBuilder(ClassName className)

创建枚举:
TypeSpec.enumBuilder("枚举名称")
TypeSpec.enumBuilder(ClassName className)

添加修饰符,public,static,final,private ,protected
addModifiers(Modifier... modifiers)

举例:

   private void test2(){
        TypeSpec myInterface = TypeSpec.interfaceBuilder("MyInterface")
                .addModifiers(Modifier.PUBLIC)
                .addField(FieldSpec.builder(String.class, "name")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                        .initializer("$S", "liming")
                        .build())
                .addMethod(MethodSpec.methodBuilder("getName")
                        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
                        .build())
                .build();

        JavaFile file1 = JavaFile.builder("com.ldx.canvasdrawdemo", myInterface).build();

        mMessager.printMessage(Diagnostic.Kind.NOTE, file1.toString()+"");

        TypeSpec myEnum = TypeSpec.enumBuilder("Range")
                .addModifiers(Modifier.PUBLIC)
                .addEnumConstant("ONE")
                .addEnumConstant("TWO")
                .addEnumConstant("THREE")
                .build();

        JavaFile file2 = JavaFile.builder("com.ldx.canvasdrawdemo", myEnum).build();

        mMessager.printMessage(Diagnostic.Kind.NOTE, file2.toString()+"");

        MethodSpec methodSpec = MethodSpec.methodBuilder("main")
                .addModifiers( javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class,"args")
                .addStatement("$T.out.println($S)",System.class,"hello world!")
                .build();
        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                .addModifiers( javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.FINAL)
                .addMethod(methodSpec)
                .build();

        JavaFile javaFile3 = JavaFile.builder("com.ldx.canvasdrawdemo", typeSpec)
                .build();

        mMessager.printMessage(Diagnostic.Kind.NOTE, javaFile3.toString()+"");

    }

生成的文件:

 package com.ldx.canvasdrawdemo;
  import java.lang.String;
  public interface MyInterface {
    String name = "liming";
    void getName();
  }
  
 package com.ldx.canvasdrawdemo;
  public enum Range {
    ONE,
    TWO,
    THREE
  }
  
 package com.ldx.canvasdrawdemo;
  import java.lang.String;
  import java.lang.System;
  public final class HelloWorld {
    public static void main(String[] args) {
      System.out.println("hello world!");
    }
  }

继承类:
.superclass(ClassName className)
实现接口
.addSuperinterface(ClassName className)
当继承父类存在泛型时,需要使用ParameterizedTypeName
ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments)

 ClassName superClass = ClassName.get("android.support.v7.app","AppCompatActivity");
        ClassName onClickClass = ClassName.get("android.view", "View.OnClickListener");

        TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TwoActivity")
                .addModifiers(Modifier.PUBLIC)
                .superclass(superClass)
                .addSuperinterface(onClickClass);

        TypeSpec mainActivity = typeSpecBuilder
                .build();

        JavaFile file = JavaFile.builder("com.ldx.canvasdrawdemo", mainActivity).build();

        mMessager.printMessage(Diagnostic.Kind.NOTE, file.toString()+"");

生成代码

package com.ldx.canvasdrawdemo;
  
  import android.support.v7.app.AppCompatActivity;
  import android.view.View.OnClickListener;
  
  public class TwoActivity extends AppCompatActivity implements View.OnClickListener {
  }

创建函数
MethodSpec.methodBuilder(String name)
创建构造方法
MethodSpec.constructorBuilder()

添加方法
addMethod(MethodSpec methodSpec)

为方法添加参数
addParameter(ParameterSpec parameterSpec)
设置返回值
returns(TypeName returnType)

举例:

   ClassName strclass = ClassName.get("java.lang", "String");

        TypeSpec myclass = TypeSpec.classBuilder("MyClass")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(strclass,"myStr")
                        .build())
                .addMethod(MethodSpec.methodBuilder("myMethod")
                         .addModifiers(Modifier.PRIVATE)
                        .returns(strclass)
                        .addStatement("return $S","name")
                        .build())
                .build();


        JavaFile file = JavaFile.builder("com.ldx.canvasdrawdemo", myclass).build();

        mMessager.printMessage(Diagnostic.Kind.NOTE, file.toString()+"");

生成的代码

package com.ldx.canvasdrawdemo;
  
  import java.lang.String;
  
  public class MyClass {
    public MyClass(String myStr) {
    }
  
    private String myMethod() {
      return "name";
    }
  }

为方法体添加代码
addCode()
addCode(String format, Object... args)
addStatement()
addStatement(String format, Object... args)

addCode 和addStatement的区别是addCode会把添加的String原封不动的作为代码,addState则会帮助添加;等操作。

JavaPoet中,format中存在三种特定的占位符:
$T 在JavaPoet代指的是TypeName,该模板主要将Class抽象出来,用传入的TypeName指向的Class来代替。
$N在JavaPoet中代指的是一个名称,例如调用的方法名称,变量名称,这一类存在意思的名称
$S & $L
$S在JavaPoet中会利用指定字符替换$S的地方,替换后的内容,$S自带了双引号,$L不自带双引号
抛出异常
.addException(TypeName name)

  MethodSpec onClick = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(override)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(viewParameter)
                    .addJavadoc("自动生成代码,勿删")
                    .addCode("String name = \"xiaoming\";\n\n")
                    .addStatement("$T good = \"\"",String.class)
                    .addStatement("$T twoView = null",viewClass)
                    .addStatement("good = name.$N()","toString")
                    .addStatement("twoView = new $T(this)",viewClass)
                    .addStatement("$T lll = $S",String.class,"lllll")
                    .addStatement("$T tttt = $L",int.class,22)
                    .build();

生成注解
AnnotationSpec.builder(override).build()

  ClassName override = ClassName.get("java.lang", "Override");

            ClassName bundle = ClassName.get("android.os", "Bundle");

            ClassName nullable = ClassName.get("android.support.annotation", "Nullable");

            AnnotationSpec overRideAnno = AnnotationSpec.builder(override).build();
            AnnotationSpec nullableAnno = AnnotationSpec.builder(nullable).build();

标识class

ClassName superClass = ClassName.get("android.support.v7.app","AppCompatActivity");
 ClassName onClickClass = ClassName.get("android.view", "View.OnClickListener");
 ClassName viewClass = ClassName.get("android.view", "View");

生成成员变量

 Class stringClazz = String.class;
 FieldSpec fieldSpec = FieldSpec.builder(stringClazz, "mName", Modifier.PRIVATE).build();
  FieldSpec fieldSpec2 = FieldSpec.builder(stringClazz, "mStr2")
                    .addModifiers(Modifier.PUBLIC)
                    .build();

设置注解
addAnnotation(AnnotationSpec annotationSpec)
addAnnotation(ClassName annotation)
addAnnotation(Class<?> annotation)

            ClassName override = ClassName.get("java.lang", "Override");
            ClassName bundle = ClassName.get("android.os", "Bundle");
            ClassName nullable = ClassName.get("android.support.annotation", "Nullable");
            AnnotationSpec overRideAnno = AnnotationSpec.builder(override).build();
            AnnotationSpec nullableAnno = AnnotationSpec.builder(nullable).build();

            ParameterSpec savedInstanceState = ParameterSpec.builder(bundle, "savedInstanceState")
                    .addAnnotation(nullable)
                   // .addAnnotation(nullableAnno)//这种方式添加也可以
                    .build();

            ParameterSpec viewParameter = ParameterSpec.builder(viewClass, "view")
                    .build();

            MethodSpec onCreate = MethodSpec.methodBuilder("onCreate")
                    .addAnnotation(override)
                   // .addAnnotation(overRideAnno)//这种方式添加也可以
                    .addModifiers(Modifier.PROTECTED)
                    .addParameter(savedInstanceState)
                    .addStatement("super.onCreate(savedInstanceState)")
                    .addStatement("setContentView(R.layout.activity_main)")
                    .build();

            MethodSpec onClick = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(override)//添加注解
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(viewParameter)
                    .addJavadoc("自动生成代码,勿删")
                    .addCode("String name = \"xiaoming\";\n\n")
                    .addStatement("$T good = \"\"",String.class)
                    .addStatement("$T twoView = null",viewClass)
                    .addStatement("good = name.$N()","toString")
                    .addStatement("twoView = new $T(this)",viewClass)
                    .addStatement("$T lll = $S",String.class,"lllll")
                    .addStatement("$T tttt = $L",int.class,22)
                    .build();

            MethodSpec test = MethodSpec.methodBuilder("test")
                    .addModifiers(Modifier.PRIVATE)
                    .addCode("int count = 0;\n")
                    .beginControlFlow("for(int i=0;i<5;i++)")
                    .addStatement("count = count + i")
                    .endControlFlow()
                    .build();

生成的代码:

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  /**
   * 自动生成代码,勿删 */
  @Override
  public void onClick(View view) {
    String name = "xiaoming";

    String good = "";
    View twoView = null;
    good = name.toString();
    twoView = new View(this);
    String lll = "lllll";
    int tttt = 22;
  }

添加注释:
addJavadoc(String format, Object... args)
.addJavadoc("自动生成代码,勿删")

流程控制:
beginControlFlow,endControlFlow会自动添加{}

   MethodSpec test = MethodSpec.methodBuilder("test")
                    .addModifiers(Modifier.PRIVATE)
                    .addCode("int count = 0;\n")
                    .beginControlFlow("for(int i=0;i<5;i++)")
                    .addStatement("count = count + i")
                    .endControlFlow()
                    .build();
 private void test() {
    int count = 0;
    for(int i=0;i<5;i++) {
      count = count + i;
    }
  }

生成文件
JavaFile负责生成最终的java文件内容,可以输出到所需地方,toString会以String输出:

JavaFile.builder(String packageName, TypeSpec typeSpec)
JavaFile通过向build方法传入PackageName(Java文件的包名)、TypeSpec(生成的内容)生成。

javaFile.writeTo(System.out)
生成的内容会输出到控制台中

javaFile.writeTo(File file)
生成的内容会存放到存储的java文件中。

  JavaFile file = JavaFile.builder("com.ldx.canvasdrawdemo", mainActivity).build();
            mMessager.printMessage(Diagnostic.Kind.NOTE, file.toString()+"");
            file.writeTo(mFilerUtils);

3 简单举例

public void createActivity(){

        try {
            ClassName superClass = ClassName.get("android.support.v7.app","AppCompatActivity");
            ClassName onClickClass = ClassName.get("android.view", "View.OnClickListener");
            ClassName viewClass = ClassName.get("android.view", "View");

            TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TwoActivity")
                    .addModifiers(Modifier.PUBLIC)
                    .superclass(superClass)
                    .addSuperinterface(onClickClass);

            ClassName override = ClassName.get("java.lang", "Override");

            ClassName bundle = ClassName.get("android.os", "Bundle");

            ClassName nullable = ClassName.get("android.support.annotation", "Nullable");

            AnnotationSpec overRideAnno = AnnotationSpec.builder(override).build();
            AnnotationSpec nullableAnno = AnnotationSpec.builder(nullable).build();

            ParameterSpec savedInstanceState = ParameterSpec.builder(bundle, "savedInstanceState")
                    .addAnnotation(nullable)
                   // .addAnnotation(nullableAnno)
                    .build();

            ParameterSpec viewParameter = ParameterSpec.builder(viewClass, "view")
                    .build();

            MethodSpec onCreate = MethodSpec.methodBuilder("onCreate")
                    .addAnnotation(override)
                   // .addAnnotation(overRideAnno)
                    .addModifiers(Modifier.PROTECTED)
                    .addParameter(savedInstanceState)
                    .addStatement("super.onCreate(savedInstanceState)")
                    .addStatement("setContentView(R.layout.activity_main)")
                    .build();

            MethodSpec onClick = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(override)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(viewParameter)
                    .addJavadoc("自动生成代码,勿删")
                    .addCode("String name = \"xiaoming\";\n\n")
                    .addStatement("$T good = \"\"",String.class)
                    .addStatement("$T twoView = null",viewClass)
                    .addStatement("good = name.$N()","toString")
                    .addStatement("twoView = new $T(this)",viewClass)
                    .addStatement("$T lll = $S",String.class,"lllll")
                    .addStatement("$T tttt = $L",int.class,22)
                    .build();

            MethodSpec test = MethodSpec.methodBuilder("test")
                    .addModifiers(Modifier.PRIVATE)
                    .addCode("int count = 0;\n")
                    .beginControlFlow("for(int i=0;i<5;i++)")
                    .addStatement("count = count + i")
                    .endControlFlow()
                    .build();

            Class stringClazz = String.class;
            FieldSpec fieldSpec = FieldSpec.builder(stringClazz, "mName", Modifier.PRIVATE).build();

            FieldSpec fieldSpec2 = FieldSpec.builder(stringClazz, "mStr2")
                    .addModifiers(Modifier.PUBLIC)
                    .build();

            TypeSpec mainActivity = typeSpecBuilder.addMethod(onCreate)
                    .addMethod(onClick)
                    .addMethod(test)
                    .addField(fieldSpec)
                    .addField(fieldSpec2)
                    .build();

            JavaFile file = JavaFile.builder("com.ldx.canvasdrawdemo", mainActivity).build();

            mMessager.printMessage(Diagnostic.Kind.NOTE, file.toString()+"");

            file.writeTo(mFilerUtils);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

生成文件:

image

package com.ldx.canvasdrawdemo;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;
import java.lang.String;

public class TwoActivity extends AppCompatActivity implements View.OnClickListener {
  private String mName;

  public String mStr2;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  /**
   * 自动生成代码,勿删 */
  @Override
  public void onClick(View view) {
    String name = "xiaoming";

    String good = "";
    View twoView = null;
    good = name.toString();
    twoView = new View(this);
    String lll = "lllll";
    int tttt = 22;
  }

  private void test() {
    int count = 0;
    for(int i=0;i<5;i++) {
      count = count + i;
    }
  }
}

其他:
匿名内部类:TypeSpec.anonymousInnerClass()

TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
    .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
    .addMethod(MethodSpec.methodBuilder("compare")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(String.class, "a")
        .addParameter(String.class, "b")
        .returns(int.class)
        .addStatement("return $N.length() - $N.length()", "a", "b")
        .build())
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addMethod(MethodSpec.methodBuilder("sortByLength")
        .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
        .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
        .build())
    .build();

生成的文件:

void sortByLength(List<String> strings) {
  Collections.sort(strings, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
      return a.length() - b.length();
    }
  });
}

更多api参阅:https://github.com/square/javapoet

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