反射


反射定义

  1. 反射式编程(英语:reflective programming)或反射(英语:reflection)或者内省

  2. 是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。[1]用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

    “通俗讲” - 通过反射技术在程序的运行过程中,来获取类/接口的信息[修饰符,类的名称,父类],属性的信息[修饰符,数据类型,属性名],方法的信息[修饰符,数据类型,名称]等.

    在运行的时候才知道我操作的是哪个类

  3. 掌握目标:

    3-1. 通过反射技术来动态获取属性的信息以及操作属性[反射的技术来对属性的值进行设置和获取]

    3-2. 通过反射技术来动态调用类中的构造方法以及获取构造方法的信息[修饰符,方法名,参数列表]

    3-3. 通过反射技术来动态获取类里面的方法的信息[修饰符,数据类型,方法名以及方法参数列表]以及反射调用方法

  4. api包 - java.lang.reflect

  5. 反射优势 - “很流氓” - 破坏封装性.

  6. 学好反射和设计模式 - 帮助我们未来来读懂一些框架源码的.所有的框架的底层都是基于反射技术来实现的.

java.lang.Class

  1. class实例 - 一个类无论被实例化多少次,那么它在JVM中的class实例永远只有1个.

  2. 它是学习反射技术的必备的类 - 提供了很多api来完成掌握目标中的动作.

  3. 所有被类加载器加载到内存中的类都是属于Class的对象 - Class类是用来描述类的类[用来描述类的元信息]

    我们的类在Class面前,就是一个Class的对象而已

反射相关api

  1. Field getDeclaredField(String name);//根据属性的名称来得到Field对象

  2. Field[] getDeclaredFields();//获取类里面所有的[包括私有的]声明的属性对应的Field数组

    只能获取到非私有的
    1. Field getField(String name);
    2. Field[] getFields();
    
  3. String getName();//获取类的全限定名

  4. String getSimpleName();//获取类的简称

  5. T newInstance();//调用空参构造

  6. Constructor getDeclaredConstructor(Class<?>… parameterTypes);

    根据指定的参数类型来得到指定的构造对应的实例Constructor对象.

    如果什么参数都不传入,拿到的就是空参构造对应的Constructor对应的实例.

  7. Constructor<?>[] getDeclaredConstructors();

    直接获取类中的所有的构造,每个构造对应一个Constructor.

  8. Method getDeclaredMethod(String methodName,Class<?>… parameterTypes);

  9. Method][] getDeclaredMethods();

获取类的Class实例的方式

  1. 类名.class

  2. 调用java.lang.Object类提供的方法Class<?> getClass();

  3. 框架底层喜欢的使用一种,更加灵活

    Class类中提供的static Class<?> forName(“类的全限定名”);//需要抓取一个非运行时异常java.lang.ClassNotFoundException类型找不到异常

  4. 基本类型.class

demo

package tech.aistar.day16;

/**
 * 本类用来演示: java.lang.Class<T>
 *
 *
 * @author: success
 * @date: 2021/8/12 10:26 上午
 */
public class ClassDemo {
    public static void main(String[] args) {
        //获取类的class实例的方式

        //每个类在JVM中的class实例永远只有1个 - 无论构建了多少个对象
        //1. 类名.class
        Class<?> c1 = Point.class;
        Class<?> c2 = Point.class;
        System.out.println(c1);//class tech.aistar.day16.Point
        System.out.println(c1 == c2);

        Class<?> c3 = String.class;
        System.out.println(c3);//class java.lang.String

        //2. 对象.getClass();
        Point p1 = new Point();
        Point p2 = new Point();

        Class<?> c4 = p1.getClass();//对象的类型
        Class<?> c5 = p2.getClass();

        // 类型判断    对象 instanceof 类
        System.out.println(c4);//class tech.aistar.day16.Point
        System.out.println(c4 == c5);//true

        //3. Class类中提供的static Class<?> forName("类的全限定名");
        //TODO... 为甚鼓励使用这种 ???
        try {
            Class<?> c6 = Class.forName("tech.aistar.day16.Point");
            System.out.println("c6:"+c6);//c6:class tech.aistar.day16.Point
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //4. 基本类型.class
        Class<?> c7 = int.class;
        System.out.println(c7);//int
    }
}

Field字段实例

java.lang.reflect.Field

api

  1. int getModifiers();
    返回由该 Field对象表示的字段的Java语言修饰符,作为整数。

    //默认的 - 0
    //public  -1
    //private - 2
    //protected - 4
    
  2. Class<?> getType();//返回属性的数据类型

  3. String getName();//属性的名称

  4. void set(Object obj,Object value);//通过属性对应的Field对象来告知JVM,应该把value设置到哪个obj对象上去.

  5. void setAccessible(boolean on);//反射操作私有属性,必须要设置为true,否则会抛出-java.lang.IllegalAccessException

  6. Object get(Object obj);//返回obj中属性字段的对应的属性值.

Constructor构造实例

调用空参构造

  1. 直接调用java.lang.Class提供的方法T newsIntance();

  2. java.lang.reflect.Constructor提供了方法

     public T newInstance(Object... initargs)
          
     Constructor<?> c1 = c.getDeclaredConstructor();
     Point p2 = (Point) c1.newInstance();//可变长列表的方法
    

调用带参构造

  1. java.lang.reflect.Constructor - 提供的方法

    public T newInstance(Object … initargs)

常用方法

  1. int getModifiers();//获取修饰符对应的数字

  2. String getName();//构造方法的名称 - [类的全限定名]

  3. void setAccessible(boolean on);//如果设置true,直接调用私有的的构造方法

    反射可以破坏单例
    

Modifier

java.lang.reflect

  1. 传入一个修饰符对应的数字,来返回修饰的具体的中文的名称

    public static String toString(int mod) {
      StringBuilder sb = new StringBuilder();
      int len;
          
      if ((mod & PUBLIC) != 0)        sb.append("public ");
      if ((mod & PROTECTED) != 0)     sb.append("protected ");
      if ((mod & PRIVATE) != 0)       sb.append("private ");
          
      /* Canonical order */
      if ((mod & ABSTRACT) != 0)      sb.append("abstract ");
      if ((mod & STATIC) != 0)        sb.append("static ");
      if ((mod & FINAL) != 0)         sb.append("final ");
      if ((mod & TRANSIENT) != 0)     sb.append("transient ");
      if ((mod & VOLATILE) != 0)      sb.append("volatile ");
      if ((mod & SYNCHRONIZED) != 0)  sb.append("synchronized ");
      if ((mod & NATIVE) != 0)        sb.append("native ");
      if ((mod & STRICT) != 0)        sb.append("strictfp ");
      if ((mod & INTERFACE) != 0)     sb.append("interface ");
          
      if ((len = sb.length()) > 0)    /* trim trailing space */
        return sb.toString().substring(0, len-1);
      return "";
    }
    

拓展应用

反射可以破坏单例

反射破坏不了枚举单例

package tech.aistar.day16;

import tech.aistar.design.singleton.version03.Singleton03;
import tech.aistar.design.singleton.version04.Singleton04;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 本类用来演示: 反射可以破坏单例
 *
 * @author: success
 * @date: 2021/8/12 2:25 下午
 */
public class ReflectPoSingleton {
    public static void main(String[] args) {
        //1. 获取单例的Class实例
        try {
            Class<?> c = Class.forName("tech.aistar.design.singleton.version03.Singleton03");
            //2. 获取空参构造对应的Constructor实例
            Constructor<?> c1 = c.getDeclaredConstructor();
            //3. 调用私有的空参构造
            c1.setAccessible(true);

            //4. 调用
            Singleton03 s1 = (Singleton03) c1.newInstance();
            //反射连续调用俩次私有的空参构造
            Singleton03 s2 = (Singleton03) c1.newInstance();

            System.out.println(s1 == s2);//false
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

为何破坏不了枚举类型单例

分析-NoSuchMethodException

//记载类,初始化静态属性,调用空参构造 
Class<?> c5 = Class.forName("tech.aistar.design.singleton.version05.Singleton05");
Constructor<?> cc = c5.getDeclaredConstructor();
控制台效果
比较繁琐的操作的事情,费时费力的事情

//不存在一个空参构造的方法让我们去调用
java.lang.NoSuchMethodException: tech.aistar.design.singleton.version05.Singleton05.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at tech.aistar.day16.ReflectPoSingleton.main(ReflectPoSingleton.java:36)

控制台

hello.java

public enum hello{
  INSTANCE
}
确认jdk-bin-jad.exe - 没有下载 - http://varaneckas.com/jad/
javac hello
jad -s java hello
反编译之后出来之后 hello(String.class,int.class)

解决方案

Constructor<?> cc = c5.getDeclaredConstructor(String.class,int.class);

分析 - IllegalArgumentException

//加载这个类
Class<?> c5 = Class.forName("tech.aistar.design.singleton.version05.Singleton05");
Constructor<?> cc = c5.getDeclaredConstructor(String.class,int.class);
//java.lang.NoSuchMethodException - 抛出一个不存在这个方法

cc.setAccessible(true);

//Exception in thread "main" java.lang.IllegalArgumentException: 
// Cannot reflectively create enum objects
Singleton05 s05 = (Singleton05) cc.newInstance();

原因剖析newInstance方法

@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
//判断是否为枚举类型,如果是枚举类型直接抛出了这个异常了.
if ((clazz.getModifiers() & Modifier.ENUM) != 0){
 throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
return inst;
}

结论 - 不允许我们用反射的技术来构建枚举类型的实例,底层会进行类型的判断,发现如果是枚举类型对应的class实例,直接抛出异常

作业

一个对象的中的所有String类型的成员变量所对应的字符串内容中的”b”改为”a”

public class Obj{
private int age = 10;

private String s2 = "b";

private String name = "tbm";

private String s1 = "adminb";

//...
}

//反射技术 - 所有字段拿出来 - Field数组中 
//获取到Field的类型,判断是不是String类型
//如果是String-"b"改为"a"

反射工厂

回忆工厂设计模式[GOF]

  1. 简单工厂方法,多方法工厂,静态方法工厂 - 本质上不属于GOF
  2. 工厂方法设计模式以及抽象工厂[专注于产品族] - 属于GOF
  3. 反射工厂 - 工厂类中利用反射技术来构建某个类的/接口的具体的实例.

补充Properties

  1. 属于集合框架的类 - 属于Map[I]

  2. java.util.Properties extends java.util.Hashtable[哈希表,多线程安全的]

  3. 作用:通过io流把本地的.properties文件读取到内存中,然后映射到Properties对象

    Properties对象就是.properties属性文件[encoding=utf-8]在内存中的映射.

    File - 本地磁盘的文件在java内存中的映射的那个对象.

  4. 存储数据的格式然后是一个键值对的形式

    # key=value - key也是不要重复的.
    username=tom
    password=123
    
  1. 根据key来获取value的方法

    String getProperty(String key);
    
  1. demo

    package tech.aistar.day16.prop;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;
    
    /**
     * 本类用来演示: 读取Properties文件
     *
     * @author: success
     * @date: 2021/8/13 8:41 上午
     */
    public enum ReadPropDemo {
        INSTANCE;
    
        // 定义一个Properties属性
        private Properties properties;
    
        ReadPropDemo(){
            //可以在构造方法 - 初始化的
            //Properties对象就是.properties属性文件[encoding=utf-8]在内存中的映射.
            properties = new Properties();
    
            //读取属性文件
            //InputStream in = new FileInputStream("src/tech/aistar/day16/prop.bean.properties");
    
            //固定的语法 - 死记住
            //获取属性文件字节输入流
            InputStream in = Thread.currentThread()
                                    .getContextClassLoader()
                                    .getResourceAsStream("tech/aistar/day16/prop/bean.properties");
    
            //加载
            try {
                //Properties对象就是.properties属性文件[encoding=utf-8]在内存中的映射.
                properties.load(in);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //普通方法
        public String getValue(String key){
            //确认properties不为null
            return properties == null?null:properties.getProperty(key);
        }
    }
    
  1. 单元测试

    package tech.aistar.day16.prop;
    
    /**
     * 本类用来演示:
     *
     * @author: success
     * @date: 2021/8/13 8:47 上午
     */
    public class ReadPropDemoTest {
        public static void main(String[] args) {
            String value = ReadPropDemo.INSTANCE.getValue("username");
            System.out.println(value);
        }
    }
    

反射工厂demo

  1. 简单工厂的缺点 - 如果有新的产品的加入,需要修改工厂类的 - 违背了软件开发的设计原则 - “开闭原则”

    优点 - 够简单

  2. 工厂方法设计模式 - 优点:一个工厂类只负责生产一个产品,如果有新的产品的加入.不需要修改工厂类,只需要增加一个工厂类

    缺点 - 项目中会存在很多的工厂类.

  3. 抽象工厂 - 负责创建一个产品族.

  4. 反射工厂既能够保证在新增一个产品的时候,能够遵守”开闭原则”,又能够保证始终仅仅只有一个工厂类.

package tech.aistar.design.factory.reflect;

/**
 * 本类用来演示: 反射工厂 - 反射工厂既能够保证在新增一个产品的时候,能够遵守"开闭原则",
 *                      又能够保证始终仅仅只有一个工厂类.
 *
 *             Properties + 反射 + 泛型方法/泛型类
 *
 * @author: success
 * @date: 2021/8/13 9:13 上午
 */
public class BaseFactory<T>{

    //面向接口编程
    //泛型方法 - 静态方法<T>,同时也要设置泛型类
    public static<T> T getInstance(String type){
        T t = null;
        //如果传入进来的是type - 是某个类的全限定名,比如
        //tech.aistar.design.factory.reflect.TeacherDaoImpl
        //获取class实例的方式
        try {
            Class<?> c = Class.forName(type);
            //反射调用空参构造
            try {
                //Point p = (Point) c.newInstance();
                t = (T) c.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return t;
    }
}

Method

java.lang.reflect.Method

常用方法

  1. int getModifiers();
    返回由该 method对象表示的字段的Java语言修饰符,作为整数。

    //默认的 - 0
    //public  -1
    //private - 2
    //protected - 4
    
  2. Class<?> getReturnType();//返回方法的返回类型

  3. String getName();//方法的名称

  4. Class<?>[] getParameterTypes();//返回方法的参数列表.

  5. Object invoke(Object obj,Object… args);//反射调用方法

  6. void setAccessible(boolean on);//调用私有方法需要调用之前来设置可见性 - true

Array

java.lang.reflect.Array - Array类提供静态方法来动态创建和访问Java数组 - 反射技术操作java数组

面试题 - Arrays和Array的区别!

java.util.Arrays - 数组工具类.

常用用法

  1. static int getLength(Object array)
    返回指定数组对象的长度,如 int 。

  2. static Object get(Object array, int index)
    返回指定数组对象中的索引组件的值。

  3. static Object newInstance(Class<?> componentType, int length)
    创建具有指定组件类型和长度的新数组。

  4. static void set(Object array, int index, Object value)
    将指定数组对象的索引组件的值设置为指定的新值。

demo

package tech.aistar.day16;

import java.lang.reflect.Array;
import java.util.Arrays;

/**
 * 本类用来演示: 反射操作java数组
 *
 * @author: success
 * @date: 2021/8/13 1:32 下午
 */
public class ArrayDemo {
    public static void main(String[] args) {
        Integer[] arr1 = {10,20,30};

        String[] arr2 = {"java","python","db","web"};

        //用一个方法,来遍历上面俩个不同元素类型数组
        //反射技术来遍历数组
        printArr(arr1);

        System.out.println("====华丽丽的分割线====");

        printArr(arr2);

        System.out.println("===反射动态创建数组===");
        //对任何元素类型的数组,都可以进行扩容操作

        //1. 确定新的数组的长度 - 原来数组的长度+扩容长度[反射创建新的数组]
        //2. 读取原来数组中的每个下标的数据[反射读取]一一复制到新的数组中[反射设置值]
        //3. 返回类型确定[扩容之后的] - Object
        Integer[] temp = (Integer[]) extendsArr(arr1);
        printArr(temp);
        System.out.println("---");
        String[] strTemp = (String[]) extendsArr(arr2);
        printArr(strTemp);

    }
    //数组的扩容
    public static Object extendsArr(Object arr){
        //1. 创建数组必不可少的俩个条件.a. 数组的元素类型  b.数组的长度
        //获取旧数组长度
        int len = Array.getLength(arr);
        //确定新的数组的长度
        int newLen = len*2;

        //2. 获取原来数组的组件的类型 - 数组的元素类型
        Class<?> type = arr.getClass().getComponentType();

        //3. 反射创建新的数组了
        Object newArr = Array.newInstance(type,newLen);

        //4. 把arr旧数组中的元素拷贝新的数组中去
        for (int i = 0; i < len; i++) {
            //根据下标获取值
            Object o = Array.get(arr,i);
            //把o设置到newArr数组的下标i处
            Array.set(newArr,i,o);
        }
        return newArr;
    }



    //反射访问
    //Integer[] String[] - 数组 - extends Object
    //并不是extends Object[]
    private static void printArr(Object arr) {
        //1. 数组的长度
        int len = Array.getLength(arr);

        //2. 遍历这个数组
        for (int i = 0; i < len; i++) {
            //反射的技术通过下标去取元素
            Object obj = Array.get(arr,i);

            System.out.println(obj);
        }
    }
}

注解

只要了解即可.jdk5.0开始引入了注解的机制.

现在框架的配置,框架的使用 - 1. 基于xml的配置方式 2. 基于注解的配置/开发方式 - 简洁

学习注解的目的是为了以后能够知道我们框架的使用中遇到注解,知道这个注解背后是个大概什么底层即可.

内置注解

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。

  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

    @SuppressWarnings(“all”) - 抑制所有的警告

  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。

自定义注解

  1. 使用@interface来修饰注解,自定义的注解默认都会继承java.lang.Annotation

  2. @Target - 指定你这个注解可以在什么地方被使用

    哪些地方 - 类,方法,参数,接口,局部变量上,属性,构造

    值可以设置成枚举类型java.lang.annotation.ElementType中的枚举常量

    public enum ElementType {
        /** Class, interface (including annotation type), or enum declaration */
        TYPE,
    
        /** Field declaration (includes enum constants) */
        FIELD,
    
        /** Method declaration */
        METHOD,
    
        /** Formal parameter declaration */
        PARAMETER,
    
        /** Constructor declaration */
        CONSTRUCTOR,
    
        /** Local variable declaration */
        LOCAL_VARIABLE,
    
        /** Annotation type declaration */
        ANNOTATION_TYPE,
    
        /** Package declaration */
        PACKAGE,
    
        /**
         * Type parameter declaration
         *
         * @since 1.8
         */
        TYPE_PARAMETER,
    
        /**
         * Use of a type
         *
         * @since 1.8
         */
        TYPE_USE
    }
    
  3. @Retention(RetentionPolicy.RUNTIME)

    可以在程序的运行过程中,通过反射的技术来得到注解的信息.

  4. 注解中只有方法的概念,没有的属性的概念

反射获取注解的值

有办法来判断类或者方法是否加入了注解

有办法通过反射技术来获取类/方法上加入注解的属性值

package tech.aistar.day16.anno;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * 本类用来演示: 反射获取注解的值
 *
 * @author: success
 * @date: 2021/8/13 2:39 下午
 */
public class MyAnnoTest {
    public static void main(String[] args) {
        //1. 判断某个类或者某个方法上是否有注解
        //有注解 - 标志 - 对这个有注解的类该干嘛干嘛...

        //注解还配置了属性值 - 需要取出属性值,然后再进行进一步的处理...

        //类
        Class<?> c = UseMyAnno.class;

        //1. 判断类上是否加入了MyAnno注解
        boolean flag = c.isAnnotationPresent(MyAnno.class);
        if(flag){
            System.out.println(c.getSimpleName()+"加入了注解!");

            //获取注解的属性值 - 前提是有
            MyAnno myAnno = c.getAnnotation(MyAnno.class);

            //获取属性值
            String[] arr = myAnno.value();

            System.out.println(Arrays.toString(arr));

        }else{
            System.out.println(c.getSimpleName()+"上没有加入注解!");
        }
        System.out.println("====方法上的注解的信息 - 反射技术===");
        try {
            Method m = c.getDeclaredMethod("add");
            if(m.isAnnotationPresent(MyAnno.class)){
                System.out.println("方法上有注解!");

                MyAnno myAnno = m.getAnnotation(MyAnno.class);

                String[] arr = myAnno.value();
                System.out.println(Arrays.toString(arr));
            }else{
                System.out.println("方法上没有注解...");
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

mybatis注解开发方法

mybatis底层使用到的是jdbc技术 - 和数据库打交道的技术

jdbc步骤很繁琐,步骤比较多~
打开
Connection con = DriverManager.getConnection(url,"root","root");
String sql="delete from user";
PreparedStatement pstmt = con.prepareStatement(sql);
 ResultSet rs = pstmt.executeQuery();
关闭
rs.close();
pstmt.close();
con.close();

mybatis就是会对jdbc的代码进行一个封装

public interface IUserDao {
 /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user")
    List<User> findAll();
}

尝试走一遍mybatis作者的路线

自定义一个注解

public @interface Select{
   String value();
}

注解本身是不会完成任何的业务逻辑的.

通过反射的技术->select * from user
  
工具类{
      Connection con = DriverManager.getConnection(url,"root","root");
    String sql="反射技术获取的";
    PreparedStatement pstmt = con.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery();
    关闭
    rs.close();
    pstmt.close();
    con.close();
}

第一阶段corejava总结

  1. JVM内存模型+GC算法 - 面试必问
  2. 静态代理和动态代理 - 设计模式

体系回顾

  1. part01-基础语法-数组的排重/排序算法/递归算[阶乘,杨辉,斐波那契数列]
  2. part02
    • 封装,继承,多态
    • 四种访问修饰符
    • static关键字,final,abstract修饰符
    • String,StringBuffer,StringBuilder
    • 包装类型Integer - [IntegerCache缓冲区,-128~127]
    • 设计模式 - 单例[双重锁检测]+工厂+模板+装饰器
    • Object - equals&hashcode,clone,toString,wait,notify,notifyAll,finalize,getClass()
    • Date&Calendar&SimpleDateFormat&BigInteger - 常用的内置的api
  3. part03 - 集合框架
    • ArrayList&LinkedList&Vector
    • HashSet&TreeSet
    • HashMap&Properties
  4. 多线程体系
  5. 反射+注解

第二阶段的知识点

周一之前 - mysql环境装好

围绕数据库

  1. mysql学习 - 关系型数据
  2. jdbc学习 - java和db进行交互的技术
  3. myabtis框架学习 - 持久层技术,对jdbc的封装

相关知识点 - mybatis-plus框架,redis - 非关系型数据


文章作者: 码农耕地人
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 码农耕地人 !
  目录