• 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.getClass()

        ③ ;通过类字面常量  : 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 = 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类型
         }
    }

    2.3 : RTTI形式总结:

      ①:传统的类型转换,比如我们在上边的demo中用到的  shape.draw();

      ②:利用Class,获取运行时信息。

      ③:得到具体的对象

    三:Java利用反射获取运行时类型信息

      如果不知道某一个对象引用的具体类型(比如已经上转型的对象),RTTI可以得到。但前提是这个类型编译器必须已知(那些是编译期不可知呢? 磁盘文件或者是网络连接中获取一串代表类的字节码)

    跨网络的远程平台上提供创建和运行对象的能力 这被称为 RMI(远程方法调用)下面会具体的介绍一下 RMI的实现方式

      反射提供了一种机制,用于检查可用的方法,并返回方法名,调用方法。

    3.1 : 获取的方式

       Java中提供了jar包 ,Java.lang.reflect 和Class对象一起对反射的概念提供支持。

    3.1.1 Java.lang.reflect :

      该类库中包含了Field Method  Constructor.这些类型的对象在JVM运行时创建,用于表示未知类里对应的成员。从而:

      ①:用Constructor创建对象,用get set读取Field内的字段

      ②:用Method.invoke()调用方法

      ③:用getFields()、getMethods()、getConstuctors() 得到与之对应的数组

    3.1.2 RTTI和RMI的区别

       检查对象,查看对象属于哪个类,加载类的class文件

      ①:RTTI会在编译期打开和检查.class文件

      ②:RMI  在编译期是 看不到.class文件。只能在运行期打开和检查.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);  
        
        //在转调具体目标对象之后,可以执行一些功能处理
      }    
    }

    3.2.2  动态代理就是:动态的创建代理并动态地处理对其所代理的方法的调用。可以参考 "彻底理解JAVA动态代理" ,"深度剖析JDK动态代理机制"。可以理解为更加灵活的代理模式

    ①  动态代理使用步骤:

      1.通过实现InvocationHandler接口来自定义自己的InvocationHandler;

      2.通过Proxy.getProxyClass获得动态代理类 

      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文件
           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();
       }
    }

    ②  :动态代理的原理,列举一下参考文献把:(本质上还是用到了反射)

    1、JDK动态代理实现原理

    2、Java动态代理机制分析及扩展

    ③  动态代理应用以及备注说明 :

      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();  
     }  
    }  

    四: Java反射在实际开发中应用

       通常,我们在一般的业务需求中是用不到反射的,但我们在更加动态的代码时,我们就可以选择反射来实现(例如对象序列化和 JavaBean)。主要的逻辑我在上边都已经说明了,所以接下来 更多的是代码展示:

        实际开发中,在运行时得到Class信息,获取method ,通过反射method.invoke()调用方法。这样做是出于AOP的设计思想。举例来说,我一个传统的web项目,我可以同过http直接传递请求给后台servlet,假如我想添加一个记录日志,或者是在请求的session中添加一个信息,如果只有一个请求,我可以直接在htttp加,但实际上请求会很多,这是我为什么在sevlet外在抽出一层,通过反射调用servlet 

        当然,很多框架其实也为我们提供了拦截的配置(这是后话)

    4.1  :在web项目中创建统一的拦截层 

    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;
        }

    4.2 : 用于webService服务 :和servlet做同意拦截,用反射去调用方法的目的一样(添加一些想要的处理,比如校验用户)。核心也是反射调用方法

        

  • 相关阅读:
    基于Python的人脸动漫转换
    let 与 var的区别
    【LeetCode】汇总
    【HDU】4632 Palindrome subsequence(回文子串的个数)
    【算法】均匀的生成圆内的随机点
    【LeetCode】725. Split Linked List in Parts
    【LeetCode】445. Add Two Numbers II
    【LeetCode】437. Path Sum III
    【LeetCode】222. Count Complete Tree Nodes
    【LeetCode】124. Binary Tree Maximum Path Sum
  • 原文地址:https://www.cnblogs.com/ldh-better/p/7148975.html
Copyright © 2020-2023  润新知