Java类型信息与应用--动态代理
本文结构
- 一、前言
- 二、为什么需要RTTI
- 三、RTTI在java中的工作原理
- 四、类型转化前先做检测
- 五、动态代理
- 六、动态代理的不足
一、前言
运行时信息使你可以在程序运行时发现和使用类型信息
Java在运行时识别对象和类的信息的方式:
1.一种是RTTI,它假定我们在编译时已经知道了所有的类型。
2.另一种是“反射“机制,它允许我们在运行时发现和使用类的信息。
这带来的好处是,你可以在程序运行时发现和使用类型信息
二、为什么需要RTTI
以多态为例,如下图基类是Shape(泛型),而派生出来的具体类有Circle,Square和Triangle。
abstract class Shape {
// this 调用当前类的toString()方法,返回实际的内容
void draw(){ System.out.println(this + "draw()"); }
abstract public String toString();
}
class Circle extends Shape {
public String toString(){ return "Circle"; }
}
class Square extends Shape {
public String toString(){ return "Square"; }
}
class Triangle extends Shape {
public String toString(){ return "Triangle"; }
}
public static void main(String[] args){
// 把Shape对象放入List<Shape>的数组的时候会向上转型为Shape,从而丢失了具体的类型信息
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
// 从数组中取出时,这种容器,实际上所有的元素都当成Object持有,会自动将结果转型为Shape,这就是RTTI的基本的使用。
for(Shape shape : shapeList){
shape.draw();
}
}
打印出以下结果
Circledraw()
Squaredraw()
Triangledraw()
RTTI在运行时识别一个对象类型,Shape对象具体执行什么的代码,由引用所指向的具体对象Circle、Square、或Triangle而决定
三、RTTI在java中的工作原理
在运行时获取类型信息是通过Class对象实现的,java通过Class对象(每个类都有一个Class对象)来执行其RTTI,而Java虚拟机通过“类加载器“的子系统,生成Class对象。
所有的类在第一次使用时,都会动态加载到JVM中。流程如下图:
Created with Rapha?l 2.1.0开始1、第一个对类的静态成员引用2、类加载器查找.class文件3、将Class对象加载进入内存4、用Class对象创建这个类的所有对象结束
class Bird{
public static final int foot=2;
static{
System.out.println("Bird coming");
}
}
class Tiger{
public static int hair=(int)(Math.random()*100);
static{
System.out.println("tiger coming");
}
}
public class Zoo {
public static void main(String[] args) throws ClassNotFoundException{
Class tiger=Class.forName("com.jaking.RTTI.Tiger");
System.out.println("Class forName初始化");
print(Tiger.hair);
//--------------------------
Bird cat= new Bird();
System.out.println("new 初始化");
print(Bird.foot);
}
public static void print(int str) {
System.out.println(str);
}
}
打印出以下结果
tiger coming
Class forName初始化
57
Bird coming
new 初始化
2
可以看出通过用forName()或是new构造器都会触发上述流程,
3.2、类字面常量
java有两种获取class对象的方法
- Class.getName(类的全限定名);
- FancyToy.class
万物皆对象,这句话在java中体现的淋漓尽致,
基本类型也可以通过方法二获取Class对象,如int.class
而通过方法二获取Class对象在加载中会有些区别,我们先看下面代码
class Dog{
public static final int foot=4;
public static final int hair=(int)(Math.random()*100);//runtime
static{
System.out.println("dog coming");
}
}
public class Zoo {
public static void main(String[] args) throws ClassNotFoundException{
Class<Dog> dog=Dog.class;//
System.out.println("类字面常量初始化");
print(Dog.foot);//compile constant
print(Dog.hair);//jiazai
}
public static void print(int str) {
System.out.println(str);
}
}
打印出了
类字面常量初始化
4
dog coming
75
“类字面常量初始化“在“dog coming“前面打印,可以看出通过方法二获取Class对象时,不会立刻初始化,而是被延迟到了对静态方法或非常量静态域进行首次引用才开始执行。
3.3、泛化的Class引用
Class引用表示的是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,可以通过泛型对Class引用所指向的Class对象进行限定,并且可以让编译器强制执行额外的类型检查:
Class intCls = int.class;
// 使用泛型限定Class指向的引用
Class<Integer> genIntCls = int.class;
// 没有使用泛型的Clas可以重新赋值为指向任何其他的Class对象
intCls = double.class;
// 下面的编译会出错
// genIntCls = double.class;
使用通配符?放松泛型的限定:
Class<?> genIntCls = int.class;
genIntCls = double.class;//编译通过
? extends Number将引用范围限制为Number及其子类
Class<? extends Number> num = int.class;
// num的引用范围为Number及其子类
num = double.class;
num = Number.class;
? super B将引用范围限制为B及其父类
class A{}
class B extends A{}
class C extends A{}
public class Zoo {
public static void main(String[] args){
Class<? super B> aClass=A.class;//将引用
Class<? super B> aClass=B.class;
//Class<? super B> aClass=C.class;//编译不通过
}
}
四、类型转化前先做检测
有些类型如果你强制进行类型转化,如果不匹配的话就会抛出ClassCastException异常,通过关键字instanceof,告诉我们对象是不是某个特定类型的实例,再执行转化。
class A{}
class B extends A{}
class C extends A{}
public class Zoo {
public static void main(String[] args){
A a=new B();
if (a instanceof B) {
a=(B)a;//上转型为B
}
}
}
五、动态代理
5.1静态代理模式
下面是展示代理结构的简单例子:
interface Subject {
public void doSomething();
}
//被代理类
class RealSub implements Subject {
public void doSomething(){
System.out.println( "RealSub call doSomething()" );
}
}
//代理类
class SubjectProxy implements Subject {
Subject subimpl = new RealSub();
public void doSomething(){
subimpl.doSomething();
}
}
public class ProxyClient {
public static void main(String[] args) {
Subject sub = new SubjectProxy();
sub.doSomething();
}
}
代理模式的类图
- 抽象主题角色(Subject):该类的主要职责是声明真实主题与代理的共同接口方法。
- 真实主题角色(RealSub):也称为委托角色或者被代理角色。定义了代理对象所代表的真实对象。
- 代理主题角色(SubjectProxy):也叫委托类、代理类。该类持有真实主题类的引用,再实现接口的方法中调用真实主题类中相应的接口方法执行,以此起到代理作用。
- 客户类(ProxyClient) :即使用代理类的类型
?代理模式又分为静态代理和动态代理。静态代理是由用户创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。动态代理是在程序运行时,通过运用反射机制动态的创建而成。
5.2、动态代理
Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这是一套非常灵活有弹性的代理框架。
5.2.1、Jdk动态代理
?Jdk的动态代理是基于接口的。现在想要为RealSubject这个类创建一个动态代理对象,Jdk主要会做一下工作:
- 获取RealSubject上的所有接口列表
- 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyN(包名与这些接口的包名相同,生成代理类的类名,格式“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率);
- 根据需要实现的接口信息,在代码中动态创建该Proxy类的字节码;
- 创建InvocationHandler实例handler,用来处理Proxy所有方法的调用;
- Proxy的class对象以创建的handler对象为参数,实例化一个proxy对象;
- Jdk通过java.lang.reflect.Proxy包来支持动态代理,在Java中要创建一个代理对象,必须调用Proxy类的静态方法newProxyInstance()获取代理对象。
下面为实例代码
package com.jaking.dynamicproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//抽象主题类B ,JDK的动态代理是基于接口的,所以一定要是interface
interface SubjectA {
public abstract void request();
}
// 抽象主题类B
interface SubjectB {
public abstract void doSomething();
}
// 真实主题类A,即被代理类
class RealSubjectA implements SubjectA {
public void request() {
System.out.println("RealSubjectA request() ...");
}
}
// 真实主题类B,即被代理类
class RealSubjectB implements SubjectB {
public void doSomething() {
System.out.println("RealSubjectB doSomething() ...");
}
}
// 动态代理类,实现InvocationHandler接口
class DynamicProxy implements InvocationHandler {
Object obj = null;
public DynamicProxy(Object obj) {
this.obj = obj;
}
/**
* 覆盖InvocationHandler接口中的invoke()方法
*
* 更重要的是,动态代理模式可以使得我们在不改变原来已有的代码结构 的情况下,对原来的“真实方法”进行扩展、增强其功能,并且可以达到
* 控制被代理对象的行为,下面的before、after就是我们可以进行特殊 代码切入的扩展点了。
*
* @param proxy
* ,表示执行这个方法的代理对象;
* @param method
* ,表示真实对象实际需要执行的方法(关于Method类参见Java的反射机制);
* @param args
* ,表示真实对象实际执行方法时所需的参数。
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
/*
* before :doSomething();
*/
Object result = method.invoke(this.obj, args);
/*
* after : doSomething();
*/
return result;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 被代理类的实例
SubjectA realSubjectA = new RealSubjectA();
// loader,表示类加载器,对于不同来源(系统库或网络等)的类需要不同的类加载器来加载,这是Java安全模型的一部分。
// 可以使用null来使用默认的加载器;
ClassLoader loader = realSubjectA.getClass().getClassLoader();
// interfaces,表示接口或对象的数组,它就是前述代理对象和真实对象都必须共有的父类或者接口;
Class<?>[] interfaces = realSubjectA.getClass().getInterfaces();
// handler,表示调用处理器,它必须是实现了InvocationHandler接口的对象,其作用是定义代理对象中需要执行的具体操作。
InvocationHandler handler = new DynamicProxy(realSubjectA);
// 获得代理的实例 A
SubjectA proxyA = (SubjectA) Proxy.newProxyInstance(loader, interfaces,
handler);
proxyA.request();
RealSubjectB realSubjectB = new RealSubjectB();
// 获得代理的实例 B
SubjectB proxyB = (SubjectB) Proxy.newProxyInstance(realSubjectB
.getClass().getClassLoader(), realSubjectB.getClass()
.getInterfaces(), new DynamicProxy(realSubjectB));
proxyB.doSomething();
// 打印生成代理类的类名
System.out.println(proxyA.getClass().getSimpleName());
System.out.println(proxyB.getClass().getSimpleName());
}
}
运行打印出
RealSubjectA request() ...
RealSubjectB doSomething() ...
$Proxy0
$Proxy1
控制台打印出$Proxy0,$Proxy1可以证明$Proxy0和$Proxy1是JVM在运行时生成的动态代理类,这也就是动态代理的核心所在,我们不用为每个被代理类实现一个代理类,只需要实现接口InvocationHandler,并在客户端中调用静态方法Proxy.newProxyInstance( )就能获取到代理类对象
下图为动态代理类$ProxyN的继承图
由图可见,
- Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。
- 被代理的一组接口有以下特点。
(1)要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。
(2)这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。
(3)需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。
(4)接口的数目不能超过 65535,这是 JVM 设定的限制。
六、动态代理的不足
动态代理的性能会比较差一些。理由很简单,因为反射地分派方法而不是采用内置的虚方法分派,可能有一些性能上的成本,但是通过动态代理可以简化大量代码,大大减低耦合度,如Spring中的AOP,Struts2中的拦截器就是使用动态代理,至于性能与便捷有时需要权衡使用。