运行时类型识别(RTTI, Run-Time Type Information)是Java中很是有用的机制,在java中,有两种RTTI的方式,一种是传统的,即假设在编译时已经知道了全部的类型;还有一种,是利用反射机制,在运行时再尝试肯定类型信息。html
本篇博文会结合Thinking in Java 的demo 和实际开发中碰到的例子,对Java反射和获取类型信息作整体上整理。文章主要分为三块:java
在学习RTTI的时候,首先须要知道Java中类是如何加载的,java又是如何根据这些class文件获得JVM中须要的信息(备注:我在此处实在是想不到更好的描述,望读者能够给出更好的描述)web
类加载器子系统包含一条加载器链,只有一个“原生的类加载器”他是jvm实现的一部分,能够用来记载本地jar包内的class,若涉及加载网络上的类,或者是web服务器应用,能够挂接额外的类加载器。spring
全部的类都是第一次使用的时候,动态加载到JVM中。建立对类的静态成员的引用,加载这个类。Java程序在开始运行的时候并不是彻底加载,类都是用的地方在加载,这就是动态加载数组
①:首先检查这个类是否被加载缓存
②:若是没有加载,再去根据类名查找.class文件,加载类的字节码,并校验是否存在不良代码,服务器
测试代码以下:cookie
//candy.java public class Candy { static { System.out.println("loading Candy"); } } //cookie.java public class Cookie { static { System.out.println("loading Cookie"); } } //Gum.java public class Gum { static { System.out.println("loading Gum"); } } //TestMain.java public class TestMain { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After create Candy"); try { Class.forName("com.RuntimeTypeInformation.Gum"); } catch (ClassNotFoundException e) { System.out.println("Could not find Class"); } System.out.println("After Class.forName"); new Cookie(); System.out.println("After new Cookie()"); } static void printClassInfo(Class c){ System.out.println("Class Name :"+c.getName() +"is interface? :" + c.isInterface() +"simple Name "+ c.getSimpleName() ); }
从输出结果能够清楚看到;class对象仅在须要的时候才会加载,static初始化是在类加载的时候进行网络
验证类中的字节码,为静态域分配存储空间。若是必须的话,将解析这个类建立的对其余类的全部引用session
若是该类存在超类,对其初始化,执行静态初始化器和静态代码块。初始化延迟至 对静态方法或者非静态方法首次引用时执行
实际开发中,需求并非一成不变的(准确来讲是常常变),而每新添加需求若是代码的改动量越小确定是越能提升效率。好比:
package com.RuntimeTypeInformation.circle; import java.util.Arrays; import java.util.List; abstract class Shape { void draw(){ System.out.println(this+".draw()"); } abstract public String toString(); } class Circle extends Shape{ @Override public String toString() { return "Circle"; } } class Triangle extends Shape{ @Override public String toString() { return "Triangle"; } } public class Shapes{ public static void main(String[] args) { //题外话,Arrays.asList 可变参数列表,能够把传入的多个对象转为一个list List<Shape> shapes = Arrays.asList(new Triangle(),new Circle()); for (Shape shape : shapes) { shape.draw(); } } }
当我想要添加一个新的形状,好比说长方形,我只须要编写一个新类继承Shape便可,而不须要修改调用的地方 。在这里用到了 ”多态“(虽然调用的都是shpe的方法,可是JVM能在运行期
准确的知道应该调用具体哪一个子类的方法)
当你第一次了解"多态",你多是简单知道堕胎就是这么一回事,那么,如今咱们去研究一下,java是怎样处理的.
① 当把Triangle,Circle 放到 List<Shape>时,会向上转型为Shape,丢失具体的类型
② 当从容器中取出Shape对象的时候,List内实际存放的是Object, 在运行期自动将结果转为Shape,这就是RTTI的工做( 在运行时识别一个对象的类型)
这时候,若是客户需求又改了,说不但愿画的结果存在圆形。应对这种需求,咱们能够采用RTTI 查询某个shape引用所指向的具体类型(具体怎么用,能够接着往下看)
Java的核心思想就是:”一切皆是对象“,好比咱们对形状抽象,获得圆形类,三角形类。但咱们 对这些类在作一次抽象,获得class用于描述类的通常特性
上图是我用画图画的(有点捞见谅),若是咱们能够拿到对象的class,咱们就能够利用RTTI获得具体的java类。至于如何拿到Class和怎样用Class获得准确的类,继续往下看。
每个类都存在与之对应的Class对象(保存在.class文件中),根据class获得具体的对象,请参考“第一章节 类的加载和初始化”
①:Class.forName("全限定类名"),获得Class对象,反作用是“若是对应的类没有加载,则会加载类”。找不到会抛出“”ClassNotFoundException”
②:若是有对象,能够直接用对象获得与之对应的Class对象 好比
Shape shape = new Circle(); shape.getClass()
③ ;经过类字面常量 : Shape.class.推荐用该方法,第一是编译器会作检查,第二是根除了对forName的调用,提升效率
方法名 | 说明 |
---|---|
forName() | (1)获取Class对象的一个引用,但引用的类尚未加载(该类的第一个对象没有生成)就加载了这个类。 (2)为了产生Class引用,forName()当即就进行了初始化。 |
Object-getClass() | 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。 |
getName() | 取全限定的类名(包括包名),即类的完整名字。 |
getSimpleName() | 获取类名(不包括包名) |
getCanonicalName() | 获取全限定的类名(包括包名) |
isInterface() | 判断Class对象是不是表示一个接口 |
getInterfaces() | 返回Class对象数组,表示Class对象所引用的类所实现的全部接口。 |
getSupercalss() | 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。 |
newInstance() | 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法建立的类,必须带有无参的构造器。 |
getFields() | 得到某个类的全部的公共(public)的字段,包括继承自父类的全部公共字段。 相似的还有getMethods和getConstructors。 |
getDeclaredFields | 得到某个类的本身声明的字段,即包括public、private和proteced,默认可是不包括父类声明的任何字段。相似的还有getDeclaredMethods和getDeclaredConstructors。 |
Class引用表示它所指向的对象的确切类型,java1.5以后,容许开发者对Class引用所指向的Class对象进行限定,也就是添加泛型。
public static void main(String[] args) { Class<Integer> intclass = int.class; intclass = Integer.class; }
这样能够在编译器进行类型检查,固然能够经过 “通配符” 让引用泛型的时候放松限制 ,语法 : Class<?>
目的:
①:为了能够在编译器就作类型检查
② : 当 Class<Circle> circle = circle.getClass(); circle.newInstance() 会获得具体的类型 。但此处需注意:
public class Shapes{ public static void main(String[] args) throws InstantiationException, IllegalAccessException { Class<Circle> circles = Circle.class; Circle circle = circles.newInstance();//第一:泛化class.newInstance能够直接获得具体的对象 Class<? super Circle> shape = circles.getSuperclass(); Object shape1 = shape.newInstance();//第二:它的父类,只能用逆变的泛型class接收,newInstance获得的是Object类型 } }
①:传统的类型转换,好比咱们在上边的demo中用到的 shape.draw();
②:利用Class,获取运行时信息。
③:获得具体的对象
若是不知道某一个对象引用的具体类型(好比已经上转型的对象),RTTI能够获得。但前提是这个类型编译器必须已知(那些是编译期不可知呢? 磁盘文件或者是网络链接中获取一串表明类的字节码)
跨网络的远程平台上提供建立和运行对象的能力 这被称为 RMI(远程方法调用),下面会具体的介绍一下 RMI的实现方式
反射提供了一种机制,用于检查可用的方法,并返回方法名,调用方法。
Java中提供了jar包 ,Java.lang.reflect 和Class对象一块儿对反射的概念提供支持。
该类库中包含了Field Method Constructor.这些类型的对象在JVM运行时建立,用于表示未知类里对应的成员。从而:
①:用Constructor建立对象,用get set读取Field内的字段
②:用Method.invoke()调用方法
③:用getFields()、getMethods()、getConstuctors() 获得与之对应的数组
检查对象,查看对象属于哪一个类,加载类的class文件
①:RTTI会在编译期打开和检查.class文件
②:RMI 在编译期是 看不到.class文件。只能在运行期打开和检查.class文件
public interface Subject { public void doSomething(); } public class RealSubject implements Subject { public void doSomething() { System.out.println( "call doSomething()" ); } } public class ProxyHandler implements InvocationHandler { private Object proxied; public ProxyHandler( Object proxied ) { this.proxied = proxied; } public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { //在转调具体目标对象以前,能够执行一些功能处理 //转调具体目标对象的方法 return method.invoke( proxied, args); //在转调具体目标对象以后,能够执行一些功能处理 } }
① 动态代理使用步骤:
1.经过实现InvocationHandler接口来自定义本身的InvocationHandler;
2.经过Proxy.getProxyClass得到动态代理类
public class MyProxy { public interface IHello{ void sayHello(); } static class Hello implements IHello{ public void sayHello() { System.out.println("Hello world!!"); } } //自定义InvocationHandler static class HWInvocationHandler implements InvocationHandler{ //目标对象 private Object target; public HWInvocationHandler(Object target){ this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代码-------------"); //执行相应的目标方法 Object rs = method.invoke(target,args); System.out.println("------插入后置处理代码-------------"); return rs; } } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); IHello ihello = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), //加载接口的类加载器 new Class[]{IHello.class}, //一组接口 new HWInvocationHandler(new Hello())); //自定义的InvocationHandler ihello.sayHello(); } }
② :动态代理的原理,列举一下参考文献把:(本质上仍是用到了反射)
③ 动态代理应用以及备注说明 :
JDK实现动态代理须要实现类经过接口定义业务方法 (接下来我会简单说一下Cglib实现动态代理)。第二是动态代理很是重要 是反射一个极其重要的模块,不少框架都离不开动态代理,好比Spring 。因此,推荐读者在多去研究一下。
④:Cglib实现动态代理
参考文档: cglib动态代理介绍(一)
CGLIB是一个强大的高性能的代码生成包。它普遍的被许多AOP的框架使用,例如spring AOP和dynaop,为他们提供方法的interception(拦截)。最流行的OR Mapping工具hibernate也使用CGLIB来代理单端single-ended(多对一和一对一)关联(对集合的延迟抓取,是采用其余机制实 现的)。EasyMock和jMock是经过使用模仿(moke)对象来测试Java代码的包。它们都经过使用CGLIB来为那些没有接口的类建立模仿 (moke)对象。
CGLIB包的底层是经过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如 Groovy和BeanShell,也是使用ASM来生成java的字节码。当不鼓励直接使用ASM,由于它要求你必须对JVM内部结构包括class文 件的格式和指令集都很熟悉
"在运行期扩展java类及实现java接口",补充的是java动态代理机制要求必须实现了接口,而cglib针对没实现接口的那些类,原理是经过继承这些类,成为子类,覆盖一些方法,因此cglib对final的类也不生效
cglib实现动态代理的demo:参考 CGLib动态代理原理及实现
这是要代理的类:
public class SayHello { public void say(){ System.out.println("hello everyone"); } }
代理类的核心
public class CglibProxy implements MethodInterceptor{ private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz){ //设置须要建立子类的类 enhancer.setSuperclass(clazz); enhancer.setCallback(this); //经过字节码技术动态建立子类实例 return enhancer.create(); } //实现MethodInterceptor接口方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置代理"); //经过代理类调用父类中的方法 Object result = proxy.invokeSuper(obj, args); System.out.println("后置代理"); return result; } }
测试结果:
public class DoCGLib { public static void main(String[] args) { CglibProxy proxy = new CglibProxy(); //经过生成子类的方式建立代理类 SayHello proxyImp = (SayHello)proxy.getProxy(SayHello.class); proxyImp.say(); } }
一般,咱们在通常的业务需求中是用不到反射的,但咱们在更加动态的代码时,咱们就能够选择反射来实现(例如对象序列化和 JavaBean)。主要的逻辑我在上边都已经说明了,因此接下来 更多的是代码展现:
实际开发中,在运行时获得Class信息,获取method ,经过反射method.invoke()调用方法。这样作是出于AOP的设计思想。举例来讲,我一个传统的web项目,我能够同过http直接传递请求给后台servlet,假如我想添加一个记录日志,或者是在请求的session中添加一个信息,若是只有一个请求,我能够直接在htttp加,但实际上请求会不少,这是我为何在sevlet外在抽出一层,经过反射调用servlet
固然,不少框架其实也为咱们提供了拦截的配置(这是后话)
doPost(..){ //这是项目中的setvlet统一的拦截层,接下来咱们看一下 actionInvoker.invoke ... else if (requestType.equalsIgnoreCase("image")) { try { ActionClassInfo actionClassInfo = actionInvoker.getClassInfo(action, request, response); actionClassInfo.setArgs(queryStringMap); Object object = actionInvoker.invoke(actionClassInfo); response.addHeader("accept-ranges", "bytes"); byte[] bytes = (byte[]) object; response.addHeader("Content-type", "application/png"); response.addHeader("content-length", String.valueOf(bytes.length)); response.getOutputStream().write(bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } catch (Throwable e) { e.printStackTrace(); } finally { response.getOutputStream().flush(); response.getOutputStream().close(); } } }
actionInvoker.invoke()方法代码以下: 在这方法内,我就能够添加我想要的处理,好比先判断是否在缓存中存在,核心的只有 method.invoke
public Object invoke(ActionClassInfo action) throws Exception { // 执行方法以前 Object cache = null; for (Object object : action.getProxys()) { if (object instanceof Intercepter){ cache = ((Intercepter) object).before(action); if(cache != null && object instanceof RedisCacheHandler){ return cache; //缓存的结果直接返回 } } } Method method = action.getMethod(); Object business = action.getClazz(); Map<Object, Object> args = action.getArgs(); method.setAccessible(true); Object result = method.invoke(business, args); // 执行方法后 for (Object object : action.getProxys()) { if (object instanceof Intercepter) result = ((Intercepter) object).after(result, action); } return result; }