初识Java Reflection

学习Java的时间也过了这么久了,反射这个名词耳熟于心,在与小伙伴讨论时也能时常提起。说来惭愧,一直以来浮在技术表面,总是以了解了某个新技术的名词为傲,但当被问起其底层实现时却无话可说。为了改变现状,同时记录自己学习过程,希望通过简书这个平台写下自己对技术的认识。接下来这篇文章我将谈谈对反射的认识、反射能获取的信息、反射的应用。那么,让我们开始挖掘反射相关的知识点吧^_^

关于反射的理解

谈及反射之前我想说说对象在Java中是怎样存在的以及存放在哪里。想必很多初学Java的小伙伴会接触到对象这个概念,Java是面向对象的语言,Java程序就是通过一个又一个对象构建起来的。我们可以通过Object o = new Object()直接生成对象,那么这个o对象是存放在哪里的呢?
如果了解过JMM的同学肯定会很清楚,对象是存放在Java堆上的,我们可以通过直接new Object()的方式在堆上生成一个对象,这种方式生成的对象是在编译时就确定了的,那么有没有可以让我们在Java程序运行时生成对象的方法?没错,通过反射我们就可以在运行期动态的生成一个对象了
反射带给我们的程序很多便利,譬如运行期间检查类、接口、变量或者方法等信息。我能通过反射做什么呢?对一个运行期对象的值进行改变,也可以拿到类方法操作对象,是不是很有趣额 ~( ̄0 ̄)/

反射获取到的信息

我认为的最重要的Class对象,有了这个对象,我们就可以为非作歹了(⊙ ▽ ⊙)哈哈不要想歪了,我只是找不到形容词来描述它了。我们可以通过如下三种方式获取一个类的Class对象:

  1. Class pigClass = Pig.class;
  2. Class pigClass = new Pig().getClass();
  3. Class pigClass = Class.forName("Pig");

注意第三种获取Class对象的方式传入的字符串需要完整的包名以及类名
既然这个Class对象那么滴重要,What can it take for us?

构造器

通过类的构造器,我们可以生成一个对象。在java.lang.reflect包下有一个Constructor类,这个类生成的实例可以存放关于构造器的一些信息。
通过上面pigClass对象获取的Constructor对象存放构造器的一些信息如下所示:

    public String pigConstructorTest() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Class<Pig> pigClass = Pig.class;
        //get constructor from pigClass
        Constructor<Pig> pigConstructor = pigClass.getDeclaredConstructor();
        //access private-constructor 
        pigConstructor.setAccessible(true);
        //new-instance through constructor
        Pig pig = pigConstructor.newInstance();
        return pig.getAge() + " " + pig.getWeight() + " " + pigConstructor.getName();
    }

这里调用的是无参构造器,所以生成的对象初始值为默认值。Constructor还有一些譬如获取构造器修饰符、构造器参数类型、构造器参数个数等方法。

字段

能够获取到类的字段,然后改变运行期对象的值是不是很cool喔(≧▽≦)/

    public void pigFiledTest() throws NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        //get class's all fields
        Field[] fields = pigClass.getDeclaredFields();
        //get one field through certain string
        Field weight = pigClass.getDeclaredField("weight");
        weight.setAccessible(true);
        for (Field f :
                fields) {
            System.out.print(f.getType() + " " + f.getName());
            System.out.println();
        }
        Pig pig = pigConstructorTest();
        System.out.print(weight.get(pig) + " after filed setting");
        weight.set(pig,1);
        System.out.print(" "+ weight.get(pig));
    }

Pig类的字段是私有的,即只有Pig对应的对象能访问,所以通过普通的pigClass.getFields()并不能获取其field数组,通过getDeclaredFields()方法即可获取其私有字段。getDeclaredFields()与getDeclaredField()的区别是一个获取所有字段,一个根据传入的字段名获取相应的field对象。Field字段对应的实例还可以在运行期间给改变对象的私有属性,上面的代码运行后结果如下:

int age
int weight
0 after filed setting 1

方法

    public void pigMethodTest() {
        //same to field,the getMethods() can not obtain private-method
        Method[] methods =
                pigClass.getMethods();
        for (Method method : methods) {
            System.out.print(method.getName() + " " + Arrays.toString(method.getParameters()));
            System.out.println();
        }
    }

同样的,要想获取类的私有方法只有通过getDeclaredMethods()。下面来看看method的一个比较重要的方法:

    public void pigMethodInvoke() throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Method setAge = pigClass.getMethod("setAge", int.class);
        Pig pig = pigConstructorTest();
        System.out.println("before method invoke:" + pig.getAge());
        setAge.invoke(pig,10);
        System.out.println("after method invoke:" + pig.getAge());
    }

对应的输出为:

before method invoke:0
after method invoke:10

可以看到,通过setAge.invoke(pig,10);我们成功地将猪的年龄设置为0。invoke方法的使用需要传入两个参数:

  1. Object obj,需要对方法进行调用的对象,如果该对象中没有该方法那么会抛出一个IllegalArgumentException:object is not an instance of declaring class
  2. Object... args,方法需要传递的实际参数。

其他

反射能够带给我们,如果一个类/方法/字段上面打上了注解,那么我们也可以通过反射拿到对应的注解内容。同上,若是一个类/方法/字段上面有泛型数据,反射能帮我们拿到已经处于运行期时候的泛型信息吗?答案是肯定的,毕竟反射这个利器能够带给我们的好处不是一星半点。反射的功能太多了这里我就不一一演示了~~~

反射的应用

我初次对反射感兴趣是在查看AOP相关的知识时,对于invoke方法里面的参数以及method.invoke()到底是个什么鬼一窍不通/(ㄒoㄒ)/~~
看过别人写的博客,了解AOP、反射相关的知识后大概有了些理解,下面来看看AOP动态代理的基于接口的实现:

public interface BookFacade {
    void addBook();
}

所有基于JDK实现的动态代理都需要一个接口,其次是具体的委托类:

public class BookFacadeImpl implements BookFacade {
    @Override
    public void addBook() {
        System.out.println("增加图书方法。。。");
    }
}

这个addBook()方法就是我们需要委托出去的方法,我们怎么将这个方法委托出去呢?需要用到一个代理类进行:

public class BookFacadeProxy implements InvocationHandler {
    /**
     * 不确定委托类是谁,被代理对象
     */
    private Object target;

    public BookFacadeProxy(Object target){
        this.target = target;
    }

    public BookFacadeProxy(){}

    /**
     * 绑定一个委托类,并返回代理类的实例
     * @param target
     * @return
     */
    public Object createProxy(Object target) {
        this.target = target;
        //需要target实现了接口
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces()
                , this);
    }
    /**
     *
     * @param proxy 代理对象
     * @param method 委托类实例的方法
     * @param args 委托类方法参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before proxy");
        //target是要调用method方法的对象,args是传入的参数
        //这里的method是BookFacade的addBook()方法
        //result返回invoke执行结果,可能为null
        Object result = method.invoke(target,args);
        System.out.println("after proxy");
        //从委托类方法调用返回的值
        return result;
    }
}

通过实现InvocationHandler类并重写其invoke方法来实现对委托类委托的方法增强。具体的看看invoke方法,

  1. 方法参数Object proxy,这个参数一般是用不到的;Method method是委托类具体想要执行的方法,Object[] args是方法传入的实参。
  2. method.invoke(target,args);这里的target就是传入的委托类实际对象。

对于这个类的createProxy()方法,其实没有必要非得在代理类里面实现,里面的Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces() , this)主要用于生成具体的代理对象,this传递的是BookFacadeProxy代理类本身的实例,不过想要调用委托类的委托方法,需要将返回的对象强转为接口类型:

public class BookFacadeTest {
    public static void main(String[] args) {
        BookFacadeProxy proxy = new BookFacadeProxy();
        BookFacade bookFacade = (BookFacade) proxy.createProxy(new BookFacadeImpl());
        bookFacade.addBook();

        BookFacadeImpl target = new BookFacadeImpl();
        BookFacade newProxyInstance = (BookFacade) Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), new BookFacadeProxy(target));
        newProxyInstance.addBook();
    }
}

可以看到只有将返回的代理类实例强转为接口类型,我们才能调用委托出去的方法。
有了动态代理,我们能够对一个实现了接口的类的方法的使用做一些增强,在方法的运行的前后做一些处理。

后记

天之道,损有余而补不足,是故虚胜实,不足胜有余。

漫漫代码路,磕磕盼盼Review,在搞技术的这条路上,希望你我都能去除浮躁内心。

推荐阅读更多精彩内容