目录

内省

简介

开发框架时,经常需要使用java对象的属性来封装程序的数据(其实就是操作对象的set/get方法来设值或取值),每次都使用反射来完成此类操作过于麻烦,所以JDK里提供了一套API,专门用于操作java对象的属性(set/get方法)。既然内省是专门用于操作java对象属性的,那首先得搞懂什么是对象的属性

对象的属性

说到属性,大家觉得很熟悉,属性不就是类里最上边的那些全局变量吗?

private String name;

private int age;

这种不都是属性吗?

其实,这是不对的!

刚才说的 private String name;private int age; 准确的来说它们应该称为:字段,而不是咱们所说的属性

那什么才是属性?

Java中的属性是指:设置和读取字段的方法,说白了就是咱们平常见到的set和get方法

只要是set和get开头的方法在java里都认为它是属性(请注意这句话,等下后边会写代码做验证)

属性名称就是set和get方法名 去掉"set"和"get"后的内容

比如:

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

它的属性名称是:name(也就是方法名称”setName”去掉“set”)

当然setName();和getName()是指同一个属性 name,

所以,咱们平常说的类里的全局变量,它并不是属性,正确的来说,它应该是字段,只不过咱们平常set和get方法写的名字和字段保持一致,所以导致大家把字段和属性认为是同一个东西

所以说白了,其实内省就是操作set和get方法的

那怎么才能得到类中的set和get方法并去操作它呢?通过反射肯定可以,但是在文章开头就已经说了,每次通过反射做的话过于麻烦,所以就出现了下边要讲的**内省(Introspector),**它就是专门做这个的,它底层也是用反射,只不过给咱们封装了,简化了操作

我们看下JDK的API文档里的 Introspector 这个类

https://gitee.com/lienhui68/picStore/raw/master/null/20200801104659.png

写代码验证一下

Student类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.eh.ftd.reflect;

import java.util.Date;

/**
 * todo
 *
 * @author David Li
 * @create 2020/08/01 09:12
 */
public class Student {

    private String name = "张三";//这是字段
    private int age;//这是字段
    private Date birthday;

    public String getName() {//这才是属性,属性指的是设置setter和读取getter字段的方法
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    //虽然上边的字段里没定义abc这个字段
    //但这也是属性:属性名称是abc,注意:只要是set或者get开头的方法都叫属性
    public String getAbc() {
        return "abc";
    }
}

测试类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.eh.ftd.reflect;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws IntrospectionException {
        //属性名称:getClass,他的属性名称class
        //getAbc --->abc
        //得到Student类中的属性,被封装到了BeanInfo中
        BeanInfo bi = Introspector.getBeanInfo(Student.class);
        //得到类中的所有的属性描述器
        PropertyDescriptor[] pds = bi.getPropertyDescriptors();
        System.out.println("属性的个数:" + pds.length);
        for (PropertyDescriptor pd : pds) {
            System.out.println("属性:" + pd.getName());
        }
    }
}

运行结果

1
2
3
4
5
6
属性的个数:5
属性:abc
属性:age
属性:birthday
属性:class
属性:name

从运行结果上来看,一共得到了5个属性,除了name,age,birthday 外还打印出了abc

上边的代码验证了咱们刚才说的:“属性其实是set、get方法”,而并不是类上边的那些字段,不然的话abc不会被打印出来的

但是name,age,birthday再加上abc应该是4个才对,那为什么会打印出5个呢?

原因很简单,因为Object类是所有类的父类,Object类里有个方法叫 getClass();

所以这也验证了咱们刚才说的: “只要是set或者get开头的方法都叫属性”。

使用内省操作属性

刚才的代码里用到了PropertyDescriptor 这个类,PropertyDescriptor顾名思义,就是属性描述之意。

它通过反射 快速操作JavaBean的getter/setter方法。 也就是说它底层也是反射去操作set和get方法,只不过它给咱们封装了,用起来更方便

PropertyDescriptor中重要的方法:

(1).写方法:getWriteMethod() – 对应set方法,它的返回值是Method对像

(2).读方法:getReadMethod() – 对应get方法,它的返回值是Method对像

代码操作一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.eh.ftd.reflect;

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

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        //Student s = new Student();
        //利用反射生成对象
        //com.eh.ftd.reflect.Student该参数可以配置到配置文件里,这才是我们想要的
        //是不是很熟悉?很多框架都是这么做的
        Class clazz = Class.forName("com.eh.ftd.reflect.Student");
        Student s = (Student) clazz.newInstance();
        PropertyDescriptor pd = new PropertyDescriptor("name", Student.class);
        Method m = pd.getReadMethod();//得到getName()方法
        String value = (String) m.invoke(s, null);//调用getName()方法
        System.out.println("调用get方法得到name的值:" + value);
        //改变name的值
        Method m1 = pd.getWriteMethod();//得到setName()方法
        m1.invoke(s, "李四");//调用setName()方法去修改name的值
        System.out.println("调用set方法改变name的值:" + s.getName());
    }
}

运行结果

1
2
调用get方法得到name的值:张三
调用set方法改变name的值:李四

这就是Java JDK里提供的内省功能(其实就是操作JavaBean里的set和get方法)

但是JDK里提供的内省还不够简单,于是乎,apache出了一套更为简单的内省框架 —— BeanUtils

BeanUtils内省操作

BeanUtils操作属性

操作之前,首先需要导入BeanUtils的jar包,以及它以来的jar包

commons-beanutils-1.8.3.jar

commons-logging-1.1.1.jar

导入jar包后,可以看到BeanUtils里有两个重要的方法:

(1).BeanUtils.getProperty(s, “name”);//调用getName方法

(2).BeanUtils.setProperty(s, “name”, “王五”);//调用setName方法

代码操作一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.eh.ftd.reflect;

import org.apache.commons.beanutils.BeanUtils;

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        //为什么要返回字符串:用户的所有输入都是字符串
        String str = BeanUtils.getProperty(s, "name");//调用getName方法
        System.out.println(str);
        //设置值
        BeanUtils.setProperty(s, "name", "王五");
        System.out.println(s.getName());
    }
}

运行结果

1
2
张三
王五

可以看到用BeanUtils操作更加的简单了

BeanUtils类型转换的问题

有个问题需要注意:BeanUtils可以进行类型的自动转换,但仅限基本类型

比如说本来需要int型,给个字符串 “28”,是可以的

但是仅限基本数据类型,像Date 这种的就不行,会报错,下边用代码体现一下:

基本数据类型自动转换

1
2
3
4
5
6
7
8
9
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        String str = BeanUtils.getProperty(s, "age");
        System.out.println(str);
        BeanUtils.setProperty(s, "age", "19");
        System.out.println(s.getAge());
    }
}

运行结果

1
2
0
19

发现ok

非基本数据类型

1
2
3
4
5
6
7
8
9
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        String str = BeanUtils.getProperty(s, "birthday");
        System.out.println(str);
        BeanUtils.setProperty(s, "birthday", "1989-10-09");
        System.out.println(s.getBirthday());
    }
}

运行结果

https://gitee.com/lienhui68/picStore/raw/master/null/20200801120751.png

发现非基本数据类型值没set进去,而且有错误提示,所以这里涉及到了BeanUtils里的String和其他类型间的互相转换的问题。要想解决这个问题,需要给BeanUtils注册一个类型转换器

代码实现一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.eh.ftd.reflect;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        //给BeanUtils注册类型转换器:自定义的转换器
        ConvertUtils.register(new Converter() {
            //type:目标类型
            //value:当前传入的值
            public Object convert(Class type, Object value) {
//             if(type.equals(Date.class)){
//                //字符串转换为Date
//             }else{
//                //Date转换为字符串
//             }
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
                if(value instanceof String){
                    //字符串转换为Date
                    String v = (String)value;
                    Date d;
                    try {
                        d = df.parse(v);
                    } catch (ParseException e) {
                        throw new RuntimeException(e);
                    }
                    return d;
                }else{
                    //Date转换为字符串
                    Date d = (Date)value;
                    return df.format(d);
                }

            }
        }, Date.class);
        BeanUtils.setProperty(s, "birthday", "1989-10-09");
        System.out.println(s.getBirthday());

    }
}

运行结果

1
Mon Oct 09 00:00:00 CST 1989

发现问题解决了。

但是这么手动的去写一个类型转换器,是不是太麻烦了,所以BeanUtils提供了Converter接口很多的实现类

其中有一个DateLocaleConverter类

所以上边的代码可以直接用DateLocaleConverter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.eh.ftd.reflect;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;

import java.util.Date;

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Student s = new Student();
        ConvertUtils.register(new DateLocaleConverter(), Date.class);
        BeanUtils.setProperty(s, "birthday", "1999-10-09");
        System.out.println(s.getBirthday());

    }
}

运行结果

1
Mon Oct 09 00:00:00 CST 1989

发现用了它提供的DateLocaleConverter类后变得很简单,DateLocaleConverter实现的功能就是咱们上面实现的

它的内部实现,其实原理都一样。

BeanUtils将Map属性自动放到Bean中

Student类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.eh.ftd.reflect;

import java.util.Date;

/**
 * todo
 *
 * @author David Li
 * @create 2020/08/01 09:12
 */
public class Student {

    private String name1;//请注意这里我写的是name1,并不是name
    private int age;
    private Date birthday;

    public String getName() {
        return name1;
    }

    public void setName(String name) {//这里的属性写的才是name,进一步验证了属性的定义
        this.name1 = name;
    }

    public int getAge() {
        return age;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "Person [age=" + age + ", birthday=" + birthday + ", name1="
                + name1 + "]";
    }

}

测试类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.eh.ftd.reflect;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 内省:操作属性的(类中的getter和setter方法)
 *
 * @author David Li
 * @create 2020/08/01 10:48
 */
public class IntrospectorDemo {
    public static void main(String[] args) throws Exception {
        Map map = new HashMap();
        //map中的key与属性一致,为了做区分请注意Person类里的字段我写的是name1,进一步验证了对属性的定义
        map.put("name", "王小二");
        map.put("age", "36");
        map.put("birthday", "1979-10-09");

        Student s = new Student();
        System.out.println("封装数据前:" + s);
        ConvertUtils.register(new DateLocaleConverter(), Date.class);
        BeanUtils.populate(s, map);
        System.out.println("封装数据后:" + s);


    }
}

运行结果

1
2
封装数据前:Person [age=0, birthday=null, name1=null]
封装数据后:Person [age=36, birthday=Tue Oct 09 00:00:00 CST 1979, name1=王小二]

可以看到值被set进去了

正如大家看到的一样,很多的框架都用到了BeanUtils这个jar包。

好了,关于Java的内省,就介绍到这,欢迎大家留言,一起讨论,学习,一起进步

参考

Java反射——内省(Introspector)以及BeanUtils内省框架