Java反射在实际开发中的应⽤
运⾏时类型识别(RTTI, Run-Time Type Information)是Java中⾮常有⽤的机制,在java中,有两种RTTI的⽅式,⼀种是传统的,即假设在编译时已经知道了所有的类型;还有⼀种,是利⽤反射机制,在运⾏时再尝试确定类型信息。
本篇博⽂会结合Thinking in Java 的demo 和实际开发中碰到的例⼦,对Java反射和获取类型信息做总体上整理。⽂章主要分为三块:
Java类加载和初始化
Java中RTTI
Java利⽤反射获取运⾏时类型信息
⼀:Java类加载和初始化
在学习RTTI的时候,⾸先需要知道Java中类是如何加载的,java⼜是如何根据这些class⽂件得到JVM中需要的信息(备注:我在此处实在是想不到更好的描述,望读者可以给出更好的描述)
1.1 类加载器(类加载的⼯具)
类加载器⼦系统包含⼀条加载器链,只有⼀个“原⽣的类加载器”他是jvm实现的⼀部分,可以⽤来记载本地jar包内的class,若涉及加载⽹络上的类,或者是web服务器应⽤,可以挂接额外的类加载器。
1.2 Java使⽤⼀个类所需的准备⼯作
1.2.1 动态加载
所有的类都是第⼀次使⽤的时候,动态加载到JVM中。创建对类的静态成员的引⽤,加载这个类。Java程序在开始运⾏的时候并⾮完全加载,类都是⽤的地⽅在加载,这就是动态加载
①:⾸先检查这个类是否被加载
②:如果没有加载,再去根据类名查.class⽂件,加载类的字节码,并校验是否存在不良代码,
测试代码如下:
//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初始化是在类加载的时候进⾏
1.2.2 链接
验证类中的字节码,为静态域分配存储空间。如果必须的话,将解析这个类创建的对其他类的所有引⽤
1.2.3 初始化
如果该类存在超类,对其初始化,执⾏静态初始化器和静态代码块。初始化延迟⾄对静态⽅法或者⾮静态⽅法⾸次引⽤时执⾏⼆:Java中RTTI
2.1 :为什么要⽤到运⾏时类型信息(就是RTTI)
实际开发中,需求并不是⼀成不变的(准确来说是经常变),⽽每新添加需求如果代码的改动量越⼩肯
定是越能提⾼效率。⽐如:
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引⽤所指向的具体类型(具体怎么⽤,可以接着往下看)
2.2 :RTTI在运⾏时如何表⽰
Java的核⼼思想就是:”⼀切皆是对象“,⽐如我们对形状抽象,得到圆形类,三⾓形类。但我们对这些类在做⼀次抽象,得到class⽤于描述类的⼀般特性
上图是我⽤画图画的(有点捞见谅),如果我们可以拿到对象的class,我们就可以利⽤RTTI得到具体的java类。⾄于如何拿到Class和怎样⽤Class得到准确的类,继续往下看。
2.3 : Class对象
每⼀个类都存在与之对应的Class对象(保存在.class⽂件中),根据class得到具体的对象,请参考“第⼀章节类的加载和初始化”
2.3.1 Class对象获取的⽅式
①:Class.forName("全限定类名"),得到Class对象,副作⽤是“如果对应的类没有加载,则会加载类”。不到会抛出“”ClassNotFoundException”
②:如果有对象,可以直接⽤对象得到与之对应的Class对象⽐如
Shape shape = new Circle();
③ ;通过类字⾯常量: Shape.class.推荐⽤该⽅法,第⼀是编译器会做检查,第⼆是根除了对forName的调⽤,提⾼效率
2.3.2: Class对象的常⽤⽅法
⽅法名说明
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。
2.3.3 泛化的Class
Class引⽤表⽰它所指向的对象的确切类型,java1.5之后,允许开发者对Class引⽤所指向的Class对象进⾏限定,也就是添加泛型。
public static void main(String[] args) {
Class<Integer> intclass = int.class;
intclass = Integer.class;
}
这样可以在编译器进⾏类型检查,当然可以通过 “通配符” 让引⽤泛型的时候放松限制,语法 : Class<?>
⽬的:
①:为了可以在编译器就做类型检查
②:当 Class<Circle> circle = Class(); wInstance() 会得到具体的类型。但此处需注意:
public class Shapes{
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Class<Circle> circles = Circle.class;
Circle circle = wInstance();//第⼀:泛化wInstance可以直接得到具体的对象
Class<? super Circle> shape = Superclass();
Object shape1 = wInstance();//第⼆:它的⽗类,只能⽤逆变的泛型class接收,newInstance得到的是Object类型
}
}
2.3 : RTTI形式总结:
①:传统的类型转换,⽐如我们在上边的demo中⽤到的 shape.draw();
②:利⽤Class,获取运⾏时信息。
③:得到具体的对象
三:Java利⽤反射获取运⾏时类型信息
如果不知道某⼀个对象引⽤的具体类型(⽐如已经上转型的对象),RTTI可以得到。但前提是这个类型编译器必须已知(那些是编译期不可知呢?磁盘⽂件或者是⽹络连接中获取⼀串代表类的字节码)
跨⽹络的远程平台上提供创建和运⾏对象的能⼒这被称为RMI(远程⽅法调⽤),下⾯会具体的介绍⼀下
RMI的实现⽅式
反射提供了⼀种机制,⽤于检查可⽤的⽅法,并返回⽅法名,调⽤⽅法。
3.1 :获取的⽅式
Java中提供了jar包,flect 和Class对象⼀起对反射的概念提供⽀持。
3.1.1 flect :
该类库中包含了Field Method Constructor.这些类型的对象在JVM运⾏时创建,⽤于表⽰未知类⾥对应的成员。从⽽:
①:⽤Constructor创建对象,⽤get set读取Field内的字段
②:⽤Method.invoke()调⽤⽅法
③:⽤getFields()、getMethods()、getConstuctors() 得到与之对应的数组
3.1.2 RTTI和RMI的区别
检查对象,查看对象属于哪个类,加载类的class⽂件
①:RTTI会在编译期打开和检查.class⽂件
②:RMI 在编译期是看不到.clas s⽂件。只能在运⾏期打开和检查.class⽂件
3.2 : 动态代理
3.2.1 我假设你对“代理模式”存在⼀定的了解(还是简单说⼀下,代理模式就是在接⼝和实现之前加⼀层,⽤于剥离接⼝的⼀些额外的操作)下⾯是代理模式的⽰例代码:
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);
java反射获取父类属性//在转调具体⽬标对象之后,可以执⾏⼀些功能处理
}
}
3.2.2 动态代理就是:动态的创建代理并动态地处理对其所代理的⽅法的调⽤。可以参考 "" ,""。可以理解为更加灵活的代理模式
①动态代理使⽤步骤:
1.通过实现InvocationHandler接⼝来⾃定义⾃⼰的InvocationHandler;
2.通过ProxyClass获得动态代理类
3.通过反射机制获得代理类的构造⽅法,⽅法签名为getConstructor(InvocationHandler.class)
4.通过构造函数获得代理对象并将⾃定义的InvocationHandler实例对象传为参数传⼊
5.通过代理对象调⽤⽬标⽅法
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⽂件
IHello ihello = (IHello) wProxyInstance(ClassLoader(), //加载接⼝的类加载器
new Class[]{IHello.class}, //⼀组接⼝
new HWInvocationHandler(new Hello())); //⾃定义的InvocationHandler
ihello.sayHello();
}
}
②:动态代理的原理,列举⼀下参考⽂献把:(本质上还是⽤到了反射)
1、
2、
③动态代理应⽤以及备注说明:
JDK实现动态代理需要实现类通过接⼝定义业务⽅法 (接下来我会简单说⼀下Cglib实现动态代理)。第⼆是动态代理⾮常重要是反射⼀个极其重要的模块,很多框架都离不开动态代理,⽐如Spring 。所以,推荐读者在多去研究⼀下。
④:Cglib实现动态代理
参考⽂档:
CGLIB是⼀个强⼤的⾼性能的代码⽣成包。它⼴泛的被许多AOP的框架使⽤,例如 AOP和dynaop,为他们提供⽅法的interception(拦截)。最流⾏的OR Mapping⼯具也使⽤CGLIB来代理单端single-ended(多对⼀和⼀对⼀)关联(对集合的延迟抓取,是采⽤其他机制实现的)。EasyMock和jMock是通过使⽤模仿(moke)对象来代码的包。它们都通过使⽤CGLIB来为那些没有接⼝的类创建模仿(moke)对象。
CGLIB包的底层是通过使⽤⼀个⼩⽽快的字节码处理框架ASM,来转换字节码并⽣成新的类。除了CGLIB包,脚本语⾔例如 Groovy和BeanShell,也是使⽤ASM来⽣成java的字节码。当不⿎励直接使⽤ASM,因为它要求你必须对JVM内部结构包括class⽂件的格式和指令集都很熟悉
"在运⾏期扩展java类及实现java接⼝",补充的是java动态代理机制要求必须实现了接⼝,⽽cglib针对没实现接⼝的那些类,原理是通过继承这些类,成为⼦类,覆盖⼀些⽅法,所以cglib对final的类也不⽣效
cglib实现动态代理的demo:参考
这是要代理的类:
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);
//通过字节码技术动态创建⼦类实例
ate();
}
//实现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 = (Proxy(SayHello.class);
proxyImp.say();
}
}
四: Java反射在实际开发中应⽤
通常,我们在⼀般的业务需求中是⽤不到反射的,但我们在更加动态的代码时,我们就可以选择反射来实现(例如对象序列化和 JavaBean)。主要的逻辑我在上边都已经说明了,所以接下来更多的是代码展⽰:
实际开发中,在运⾏时得到Class信息,获取method ,通过反射method.invoke()调⽤⽅法。这样做是出于AOP的设计思想。举例来说,我⼀个传统的web项⽬,我可以同过http直接传递请求给后台servlet,假如我想添加⼀个记录⽇志,或者是在请求的session中添加⼀个信息,如果只有⼀个请求,我可以直接在htttp加,但实际上请求会很多,这是我为什么在sevlet外在抽出⼀层,通过反射调⽤servlet
当然,很多框架其实也为我们提供了拦截的配置(这是后话)
4.1 :在web项⽬中创建统⼀的拦截层
doPost(..){
//这是项⽬中的setvlet统⼀的拦截层,接下来我们看⼀下 actionInvoker.invoke
...
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论