天天看点

Java基础 -- 05反射reflect反射:

引序:开始反射概念学习之前,先来段引序。

请牢记:计算机的三大程序结构;顺序执行结构、条件分支结构、循环处理结构。

有时我们写的程序会自我感觉这里写了代码,它是怎么被调度执行的呢,按照程序的顺序执行来考究,有点说不通。我举几个例子说明看起来说不通的现象:

  • 说不通1:注解
import java.lang.reflect.*;
import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnno{	                                     //注解定义
    String name() default "12345";
    int value();
}
 
@MyAnno(name="haha",value=123)                              //注解钉点
class B {
 
    public static void main(String[] args){
		
	if( B.class.isAnnotationPresent(MyAnno.class) ){
	    Annotation annos[] = B.class.getAnnotations();  //消费注解
	    for(Annotation anno : annos){
		System.out.println(anno);
		System.out.println( ((MyAnno)anno).name() );
		System.out.println( ((MyAnno)anno).value() );
                System.out.println( anno.annotationType().getName() );
}}}}
           

上面注解的小示例:如果看《注解定义》《注解钉点》 这两个地方,能搞懂我们写这两处的意思嘛?完全是懵圈的,这是什么代码,又什么时候被调度执行,咋执行的呢?正是因为有 《注解消费》 这个梗,我们才恍然大悟,注解是啥时用起来的。(这也是很多框架利用大量注解的底层技术手段,第三方框架有其自己的《注解定义》以及《注解消费》底层,开放给我们开发时用的就是“注解钉点”,我们利用好《注解钉点》这一点就OK)

  • 说不通2:匿名内部类

匿名内部类:作为方法的入参来使用,例如:

1:事件监听处理器类:处理事件时作为监听处理器方法的形参的Handler类

event.addEventListener( new Handler() { //这里的Handler就是匿名内部类

    public void xxxMethed(Event event){ //这是匿名类中的方法xxxMethod();
    }

}); //这个xxxMtd()是由事件调度线程来执行的。
           

以上代码我们只是调用了addEventListener(Handler hd); 传进来一个hd对象而已,那么Handler类中的xxxMethod()方法怎么会被调用的呢?答:是被事件调度线程来执行的。事件调度线程是JVM管理的,不受我们的代码控制。

2:多线程编程时的任务类:往线程池当中提交一个实现了Runnable接口的类

threadPoolExecutor.execute( new Runnable(){ //这里的Runnable就是匿名内部类

    public void run(){                      //这个匿名类中有一个方法run();
    }

}); //这个run()是由线程池调度工作线程来执行的。
           

以上代码只是调用了execute(Runnable task); 传进来一个task任务而已,那么Runnable类中的run()方法怎么会被调用的呢?答:是被线程池调度工作线程来执行的。线程池是一个框架,它怎么调度和管理工作线程,不受我们的代码控制。我们“写好匿名内部类中的方法业务逻辑”这一点就OK。理解匿名内部类很重要,它是静态代理模式的一种体现。之所以匿名内部类很重要,是因为它是理解lambda函数式编程思想的基础。

  • 说不通3:动态代理
import java.lang.reflect.*;

//接口
interface InterfaceHello {
    void sayHello();
    void read();
}

//业务类:实现接口
class HelloImpl implements InterfaceHello {
    public void sayHello() {
        System.out.println("hello world");
    }
    public void read() {
        System.out.println("I am reading...");
    }
}

//我是代理:业务类真正业务逻辑的前后,我搞点事(比如:打个广告)
class ProxyHello implements InvocationHandler{

    private Object target;

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

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("我是代理,看视频之前我打个广告,haha!");

        Object obj = method.invoke(target,args); // 真正的业务逻辑

	System.out.println("我是代理,视频看完给评价下呗,多谢!\r\n");
        
        return obj;
    }
}

//测试
public class HelloTest {

    public static void main(String[]args) {
 
        try{
	    InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(
		    HelloTest.class.getClassLoader(),
                    new Class<?>[]{InterfaceHello.class},
                    new ProxyHello(new HelloImpl())
	    );
        
	    h.sayHello(); //调用InterfaceHello接口中的sayHello()方法
	    h.read(); //调用InterfaceHello接口中的read()方法
	}
	catch(Exception e){}
    }
}
           

程序输出:

我是代理,看视频之前我打个广告,haha!

hello world

我是代理,视频看完给评价下呗,多谢!

我是代理,看视频之前我打个广告,haha!

I am reading...

我是代理,视频看完给评价下呗,多谢!

如果单看以上的测试类:h.sayHello(); 这句代码,是蒙圈的,因为按照我们以往的程序经验,这不就是实现了InterfaceHello接口的HelloImpl干的事嘛,但是程序的输出却不是这样,前有广告,中间是正事,后有评论。为什么这样呢?是因为:h是Proxy.newProxyInstance()出来的,下面简要分析下:

Proxy.newProxyInstance(loader,intfs[],handler){

 Class<?> cl = getProxyClass0(loader, intfs); //内存中生成一个类$Proxy0,实现intfs所有接口

 final Constructor<?> cons = cl.getConstructor(constructorParams); //类$Proxy0的构造函数

 return cons.newInstance(new Object[]{handler}); //引入handler,new $Proxy0对象返回

}

那这个内存中动态生成的类$Proxy0到底长什么样子呢?我写个简易版的样子:

    public class $Proxy0{

        private InvocationHandler handler;

        public $Proxy0(InvocationHandler handler){

            this.handler = handler;

        }

        public void sayHello(){ //这就是动态代理,【魔术揭秘】的地方

            handler.invoke();

        }

        public void read(){

            handler.invoke();

        }

    }

看:InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(loader,intfs[],handler);

我们恍然大悟,我们拿到的是$Proxy0对象,这个对象实现了所有的接口,所以向下类型强转后,我们拿到的变量h可以调用接口中的任意方法。我们代码随后编写的 h.sayHello(); 这下彻底明白,它的内部逻辑是调用handler.invoke();//看上方的【魔术揭秘】。所以我的总结是:程序的顺序执行结构永远是这么个套路,大家别以为这不是废话嘛,程序不顺序执行难道还来回胡乱的乱跑执行嘛?不不不,要深刻体会,有助于碰到新技术时的理解的。在用很多第三方框架的时候,我们写好“业务实现逻辑”就OK,代理都是框架帮我们做好的,比如我们的方法业务逻辑里面并没有事务管理,但CRUD增删查改时都是能反应到数据库并提交/回滚的。

总结:以上三个小示例不过是解释了随着编程技术的更多手段,让我们对程序总是顺序执行结构在感官上的认知有所偏差。其实呢?说到底,程序的执行永远是从上至下的顺序执行结构,只不过有些代码比较显而易见,有些代码并不显而易见是由于框架/JVM等有一些我们看不见的底层机制,来调度顺序执行我们的代码。所以很多时候我们是了解底层原理即可,(依托底层机制抛出来的)面向应用高层面的一些API来编写好代码即可,必定我们不是java公司要制作jvm或者api,我们只是利用api写应用程序。

反射:

官方定义,反射是程序在运行时分析代码的能力(更贴切的说,就是知晓类结构信息的能力)。结合上述三个小示例的演示和说明,反射不过就是一种编程手段而已,就这么简单。我给反射下的定义是:“知晓类结构”

  • 强调:

类对象 Class<?> clazz = myObj.getClass(); 这个clazz就是类对象。每一个类文件被JVM加载后,JVM就自动的创建好了一个类对象clazz,这个类对象包装了这个类的结构信息,一个类大致的结构:就是它实现了什么接口,它继承了哪个超类,它的构造函数,它的数据成员,它的方法成员,方法成员的入参有几个都是什么类型,大致一个类的结构就这么点玩意。正是由于JVM自动创建出来一个类对象,所以我们才能写出来clazz.getFields(); //知道类的数据成员有哪些,clazz.getMethods(); //知道类的方法成员有哪些,等等API。如果这种技术手段给一个更贴切的汉字含义,其实应该叫“知晓类结构”,不过这个名字太土了点,java就给它起了个高大上的名字:反射。

  • 又强调:

知道了类对象的存在,那么现在大家都应该理解所谓的静态成员为什么都是 类名.静态成员名 的访问手法了,这看起来像什么呢?不就是 对象.方法名 的写法嘛,这里的类名其实类对象的意思。

  • 再三强调:

请看下方示例代码,为什么静态方法上的synchronized和非静态方法上的synchronized 并不会互相干扰呢?因为它们的内置锁不同。静态方法的内置锁是类对象,而非静态方法(/也叫对象方法)的内置锁是具体的new出来的对象,它们不是同步一把锁,所以静态方法和对象方法并不会互斥。

class A{

    //我是静态方法,我的内置锁是A类对象
    public synchronized static void a1(){

        /**我在执行
         * 此时想调用a2(), a2()被互斥
         * 此时想调用b1()或者b2()是可以的
         */
    }

    //我是静态方法,我的内置锁是A类对象
    public synchronized static void a2(){}

    //我是对象方法,我的内置锁是当前调用我的对象
    public synchronized void b1(){
        
        /**我在执行
         * 此时想调用b2(), b2()被互斥
         * 此时想调用a1()或者a2()是可以的
         */
    }

    //我是对象方法,我的内置锁是当前调用我的对象
    public synchronized void b2(){}    
}
           

以上是对Class类对象的一个浅显的总结,下面我们接着回到话题:反射。

类对象:知晓类结构

类对象clazz 知晓类结构信息(包括继承父类的)public修饰的

Class<?> clazz = MyClass.class;                           //得到类对象clazz 第1种手段

Class<?> clazz = myObj.getClass();                      //得到类对象clazz 第2种手段

Class<?> clazz = Class.forName(pkg.MyClass);   //得到类对象clazz 第3种手段

方法 方法返回值 说明(/知晓类结构)

实现接口 getInterfaces() Class<?>[ ] 类实现的所有接口
继承父类 getSuperclass() Class<? extends T> 类所继承的父类
构造函数 getConstructors() Constructor<?>[ ] 类所有的构造函数
getConstructor(Class<?>...paramTypes) Constructor<T> 类的某个构造函数
数据成员 getFields() Field[ ] 类所有的数据成员
getField(String name) Field 类的某个数据成员
方法成员 getMethods() Method[ ] 类所有的方法成员
getMethod(String name,Class<?>...paramTypes) Method 类的某个方法成员
注解成员

最下方有个注解的单独表格。

Class类、Field字段、Method方法、Constructor构造函数、都支持注解的。

类对象clazz 仅仅知晓类自身,不包括继承来的,Declared*方法

类自身的,不包括继承 方法 方法返回值

内部成员

所属的声明类

getDeclaringClass() Class<?>
构造函数 getDeclaredConstructors() Constructor<?>[ ]
getDeclaredConstructor(Class<?>...paramTypes) Constructor<T>
数据成员 getDeclaredFields() Field[ ]
getDeclaredField(String name) Field
方法成员 getDeclaredMethods() Method[ ]
getDeclaredMethod(String name,Class<?>...paramTypes) Method

通过以上对Class类的部分方法(这里集中出了在编写反射代码时常用的一些API)的总结,可以知晓,通过clazz类对象调用这些API我们可以做到知晓类结构的信息。比如:知道类中有哪些属性,哪些方法等。

类结构之:内部成员

类对象 Class<?> clazz = myObj.getClass(); 这个clazz就是类对象,通过调用类对象的API,我们可以获知类的结构,其内部成员我主要从以下三个着手讲解:构造函数、数据成员、方法成员。如图:

Java基础 -- 05反射reflect反射:

内部成员--构造函数

Constructor<T> cons = clazz.getConstructor(Class<?>...paramTypes);

T obj = cons.newInstance(initargs[]); //平时主要用构造函数的这句代码来生成一个对象

内部成员--数据成员

Field f = clazz.getDeclaredField(String fieldName);

f.setAccessible(true); //意指能访问f字段的值(默认反射只能访问public修饰的)

f.getAnnotationByType(MyAnno.class); //字段除了值以外能携带额外信息,注解

内部成员--方法成员

Method m = clazz.getMethod(String mtdName, Class<?>...paramTypes); //参数的类型

m.invoke(Object target, Object...paramArgs); //实际的参数

静态代理:

静态代理要求:接口,实现该接口的两货色:真实角色,代理角色;代理角色要持有真实角色的引用。静态代理的本质是运用java的多态性的体现模式一,方法重写。java多态性,请参考我的博客 Java基础 -- 02类 /接口 /继承

静态代理模式:匿名内部类就是一种静态代理模式的体现。lambda表达式又是匿名内部类的一种“语法糖”的简易版本,所以lambda表达式也是一种静态代理模式的体现。

静态代理这个简单,一个小示例:

//接口
interface Hello {
    void say();
}

// 真实角色
class HelloImpl implements Hello {
    
    public void say() {
        System.out.println("..我是业务实现方!");
    }
}

// 代理角色
class ProxyH implements Hello {
    
    private Hello hImpl; // 持有真实业务方的引用
    
    public ProxyH(){}
    public ProxyH(Hello hImpl){ this.hImpl = hImpl; }

    public void say() {

        System.out.println("代理干的事:业务开始前,来段广告~~");

        hImpl.say(); //真实业务执行

        System.out.println("代理干的事:业务结束后,下评论呗^^");
    }
}


//测试
public class StaticProxyTest {

    public static void main(String[] args) {
        
        Hello hImpl = new HelloImpl(); //创建真实业务
        
        Hello proxyH = new ProxyH(hImpl); //创建静态代理

        proxyH.say(); //代理执行
    }
}
           

静态代理小示例执行结果:

代理干的事:业务开始前,来段广告~~

..我是业务实现方!

代理干的事:业务结束后,下评论呗^^

动态代理:

Java基础 -- 05反射reflect反射:

在本文刚开始的引序里面的3,动态代理,已经说的很详细。这里只是总结下:

动态代理--核心技术:

运行时JVM动态生成内存中的代理类 $Proxy0

    public class $Proxy0{

        private InvocationHandler handler;

        public $Proxy0(InvocationHandler handler){

            this.handler = handler;

        }

        public void sayHello(){ //这就是动态代理,【魔术揭秘】的地方

            handler.invoke();

        }

    }

动态代理--解决问题:

在不影响原有代码结构的情况下(有的时候也影响不了,比如:别人封装好的jar包,无法反编译的,我们就无法窥探class文件所对应的源码java文件到底长什么样,自然无法对其进行修改扩容。当然此话不绝对,比如Hibernate架构就是通过CGLIB技术分析class文件的字节码来动态的在内存中生成代理类的,只是这种手段一般程序员无法掌握而已,太复杂),我们又想在原有代码做业务行为的前后横加干涉一下,比如穿插广告,事后提醒评论,记录日志等,那这时用动态代理就很合适。

动态代理--使用场景:

日志系统,Spring的AOP技术,声明式事务管理,等等。Hibernate用CGLIB技术实现动态代理,Spring用Proxy技术实现动态代理。

反射配套注解,更强悍:

举一个简单的说明大家即可明白,我们通过反射能知道类的数据成员有 id,name,age等等,但是通过反射Field的若干方法,我们能知道哪个是主键嘛?这玩意单单靠反射Field的API达不到要求。我们回想一下,注解,干嘛的呢?注解就是个标签(标签可以携带额外信息),注解钉点在哪里,当前被钉点的元素(可以是类/字段/方法/构造函数等)就能携带更多的额外信息,然后我们简单的针对当前元素调用出注解field.getAnnotations(MyAnno.class)来消费,就能很好的在这么多的字段中,判别出哪个字段才是主键。注解不理解的话,可以参考我的博客 Java基础 -- 06注解Annotation

反射结合泛型再配套注解,更更更强悍:

Java基础 -- 05反射reflect反射:
Java基础 -- 05反射reflect反射:

以上两个图,是jdk5之后,java为了迎合到底怎么处理泛型而扩展出来的类型。从jdk5之后,java有了这些类型:

  • 8种基本数据类型、
  • Class类类型、
  • ParameterizedType泛型、
  • TypeVariable泛型变量类型、
  • AnnotatedArrayType泛型(/泛型变量)数组类型

上图中的配套小示例以及不了解泛型的话,请参考我的博客 Java基础 -- 04泛型

为什么说,更更更强悍呢?先看小示例:

继续阅读