反射是一种内置机制,用于在执行时对Java程序进行内省。它可以用来检查、修改和转换Java程序,而不会影响现有代码。这个强大的特性可以用来扩展程序的功能,在运行时检查类或对象的内部结构。本文试图探究其中的一些错综复杂之处,并对其有效使用略作一瞥。

如何有效地使用Java反射插图

 

反射API

反射API是标准java api库的一部分。它使我们不仅可以探索类的内在本质,而且可以在运行时不使用显式的new运算符来实例化一个类。使用这个API,执行代码可以向对象请求它的类,研究它的方法、它所接受的参数、返回类型和构造函数。这是动态完成的;编译器不知道运行时加载的类。

API托管在java.lang.reflect.package。从结构上讲,api与Java语言没有区别,它包含所有的语言元素,如类、方法、字段、构造函数、接口、注释等等。

名为Class<?>是反射API中最重要的类,是内省和反射机制的基础。它是一个final类;因此,它不能被扩展。

public final class Class<T> extends Object implements
   Serializable, GenericDeclaration, Type, AnnotatedElement

加载到Java运行时的任何类都会创建一个类的实例,该实例表示加载的类或接口。众所周知,Object类是Java中所有类的父类;它提供了一个名为getClass()的方法。我们可以使用此方法获得实例化类对象的引用。或者,类中定义的静态forName()方法也可用于加载未知类并获取其类对象的引用。类类提供一组方法来获取类名、它的方法、修饰符、构造函数等等。例如:

  • 类的getName()方法可用于获取它所表示的类的完全限定名。
  • getModifiers()方法返回Java语言修饰符;
  • getConstructors()方法返回一个数组,其中包含反映该类对象所表示类的所有公共构造函数的构造函数对象。
  • 方法的作用是:返回一个Field对象数组,该数组反映由该类对象表示的类或接口声明的所有字段,依此类推。

例如,我们可以很容易地列出Object类的所有public方法,如下所示:

Method[] methods = Object.class.getMethods();
for(Method method:methods){
   System.out.println(method.toString());
}

类似地,我们可以检索Integer类的字段信息。

Field[] fields = Integer.class.getFields();
for(Field field:fields){
   System.out.println(field.toString());
}

newInstance()方法用于实例化当前类所表示的对象。如果类或无参数构造函数不可访问,则抛出IllegalAccessException。通常,如果类表示抽象类、接口、数组类、基元类型、Void,或者它没有默认的或由程序员提供的无参数构造函数,它会抛出另一个异常,称为InstantiationException

try {
   Constructor<String> constructor =
      String.class.getConstructor(String.class);
   String strObj = constructor.newInstance("Hello");
   Method method = String.class.getMethod("length");
   int len = (int)method.invoke(strObj);
   System.out.println(len);
}catch (NoSuchMethodException | IllegalAccessException
   | InvocationTargetException | InstantiationException ex){
   ex.printStackTrace();
}

注解反射

Java应用程序可以使用反射API对注解进行反射。这些api有助于在运行时检查代码中存在的注释元素,并根据需要修改它们。例如,我们可以使用API来发现和反思类定义中声明的特定注释,如下所示:

package com.mano.examples;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public@interface DemoAnnotation {
   // ...
}

package com.mano.examples;
@DemoAnnotation
public class DemoClass {
   // ...
}

我们可以通过下面的方法来确定注释@DemoAnnotation是否应用于DemoClass

DemoAnnotation annotation = DemoClass.class
   .getAnnotation(DemoAnnotation.class);
if(annotation==null){
   System.out.println("DemoAnnotation is not applied");
} else {
   System.out.println("DemoAnnotation is applied");
}

因为Java中注释的使用非常丰富,比如用于restfulweb服务的java api、Java持久化、JMSBean验证等等。我们可以有效地使用反射API来获取有关运行应用程序中使用的注释的信息。

泛型反射

反射API还支持泛型类型的内省。通过使用这些api,我们可以发现类或任何其他元素使用的泛型参数的类型。假设有一个类,如下所示:

package com.mano.examples;
public class Invoice implements Payable {
   private String itemNumber;
   private String itemName;
   private int quantity;
   private double unitPrice;
   public Invoice(String itemName, int quantity,
         double unitPrice) {
      // ...
   }
   public int getQuantity() {
      // ...
      return quantity;
   }
   public double getUntPrice() {
      // ...
      return unitPrice;
   }
   @Override
   public double calcAmount() {
      return getQuantity()*getUntPrice();
   }

}

package com.mano.examples;
import java.util.List;
public class DemoClass {
   private List<Invoice> invoices;
   public List<Invoice> getInvoices(){
      return invoices;
   }
}

我们可以考虑使用Invoice type参数声明为泛型类型ListInvoice类的属性。

try {
   Type type = DemoClass.class
      .getDeclaredField("invoices").getGenericType();
   if(type instanceof ParameterizedType){
      ParameterizedType parameterizedType =
         (ParameterizedType) type;
      for(Type t:parameterizedType.getActualTypeArguments()){
         System.out.println(t);
      }
   }
}catch (NoSuchFieldException ex){
   ex.printStackTrace();
}

使用反射修改应用程序代码

有了反射API,我们可以以某种方式修改代码。例如,让我们声明一个带有私有字段和一个公共getter类的类,如下所示:

package com.mano.examples;
public class SampleClass {
   private String greetings;
   public String getGreetings(){
      return greetings;
   }
}

请注意,该类只有getter方法,而没有setter方法来设置字符串字段问候语。这意味着我们无法在运行时设置任何问候语的值。但是,有趣的是,我们可以使用反射API,更改字段的可见性和可访问性范围,并将字段设置为所需的值,如下所示。

Try {

   SampleClass sampleClass = new SampleClass();
   Field field = SampleClass.class
      .getDeclaredField("greetings");
   field.setAccessible(true);
   field.set(sampleClass,"Hello, everybody. I am all set.");
   System.out.println(sampleClass.getGreetings());

}catch(NoSuchFieldException | IllegalAccessException ex){
   ex.printStackTrace();
}

注意字段集可访问field.setAccessible(true)必须设置;否则,我们无法访问声明为私有成员的任何类成员。这个特性对于实现依赖注入框架特别有用。

反射安全问题

了解getMethods()getConstructors()方法都会抛出SecurityException。这意味着SecurityManager在幕后使用安全策略来确定不安全或敏感的操作。例如,当调用checkMemberAccess(this, Member.PUBLIC)方法时,它拒绝访问类中的方法和构造函数,这表明具有公共访问权限的方法或构造函数不能通过动态发现调用。当在当前类的类装入器的类层次结构中找不到调用方的类装入器时,也会引发SecurityException。这实际上意味着加载的类不是它所属包的一部分。这会引发安全漏洞,并拒绝对已加载类的包权限。

反射API的缺陷

不管怎样,反射api都有安全问题,并且可能不适用于应用程序运行的每个编程环境。它还可能对应用程序的性能产生负面影响。从这个意义上说,API是重而昂贵的。不能保证型号安全。程序员在大多数情况下被迫使用对象实例。此外,它在转换方法或构造函数参数和方法返回值方面也非常有限。

关于反射性能的问题可以参考之前的这篇文章:https://javakk.com/733.html

结论

本文只是简单介绍了反射在Java应用程序中的适用性。反射并不是每种编程语言中都存在的常见现象。Java支持这个特性,并且从版本1开始就是它的一部分。除了许多框架之外,还有许多库广泛使用反射api。可能是我们在任何现代java IDE中最常见的应用程序,它在输入Java代码时主动提供类成员的详细信息。它功能强大,是Java最重要的特性之一。