java基础强化——深入理解反射
使用过Spring框架进行web开发的应该都知道,Spring的两大核心技术是IOC和AOP。而其中IOC又是AOP的支撑。IOC要求由容器来帮我们自动创建Bean实例并完成依赖注入。IOC容器的代码在实现时肯定不知道要创建哪些Bean,并且这些Bean之间的依赖关系是怎样的(如果写死在里面,这框架还能用吗?)。所以其必须在运行期通过扫描配置文件或注解的方式来确定需要为哪些类创建实例。通俗的说,必须在运行时为编译期还不能确定的类创建实例。再直白一点,必须提供一种new Object()之外的创建对象的方法。依赖注入存在类似的问题,容器必须能够在运行时发现所有标注有@Autowired或@Resource的字段或方法,并且能够在不知道对象的任何类型信息的情况下调用其setter方法完成依赖的注入(默认bean的字段都会实现setter方法)。总结一下IOC容器在实现时必须做到的三件看起来“不太可能的事”。
- 1.提供new之外的创建对象的方法,这个对象的类型在编译期不能确定。
- 2.能够在运行期知道类的结构信息,包括:方法,字段以及其上的注解信息等。
- 3.能够在运行期对编译期不能确定任何类型或接口信息的对象进行方法调用。
而这些,在java的反射技术下成为了可能。应该说反射技术并不仅仅在IOC容器中被使用,它是整个Spring框架的底层核心技术之一,是Spring实现通用型和扩展性的基石。
2. 反射技术初探
2.1 什么是反射技术
下面这段是官方的定义
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
总结下官方的定义,我们可以知道反射技术的核心就两点:
- 1.反射使程序能够在运行时探知类的结构信息:构造器,方法,字段等。
- 2.并且依赖这些结构信息完成相应的操作,比如创建对象,方法调用,字段赋值等。
2.2 类结构信息和java对象的映射
回顾下类加载的过程:当JVM需要使用某个类,但内存中不存在时,会将该类的字节码文件加载进内存的方法区中,并在堆区创建一个Class对象。Class对象相当于存储于方法区的字节码信息的映射。我们可以通过该Class对象获得关于该类的所有描述信息:类名,访问权限,类注解,构造方法,字段,方法等等,尽管真实的类信息并不存在于该对象中。但通过它我们能获得想要的东西,并进行相关的操作,某种程度可以认为它们逻辑上等价。对类的结构进一步细分,类主要由构造方法,方法,字段构成。所以也必须存在和它们建立逻辑关系的映射。java在反射包下定义了Constructor类,Method类和Field类来建立和构造方法,方法,字段的映射。Constructor对象映射构造器方法,Method对象映射静态或实例方法,Field对象映射类的字段,而Class对象映射整个字节码文件(从字节码文件中抽取的方法区的运行时数据结构),通过Class对象又可以获得Method,Constructor,Field对象。它们之间的关系如下图所示。
通过这个图像我们对反射可以建立更加直观的认识。堆中的对象就像一面镜子,反射出类全部或某一部分的面貌。通过这些对象,我们可以在运行时获取类的全部信息;并且同样通过这些对象,可以完成创建类的实例,方法调用,字段赋值等操作。
3 Class对象的获取及需要注意的地方
我们知道Class对象是进行反射操作的入口,所以首先必须获得Class对象。除了通过实例获取外,Class对象主要由以下几种方法获得:
- 1.通过类加载器加载class文件
Class<?> clazz = Thread.currentThread().getContextClassLoader().
loadClass("com.takumiCX.reflect.ClassTest");
- 2.通过静态方法Class.forName()获取,需要传入类的全限定名字符串作参数
Class<?> clazz = Class.forName("com.takumiCX.reflect.ClassTest");
- 3.通过类.class获得类的Class对象
Class<ClassTest> clazz = ClassTest.class;
除了获得的Class对象的泛型类型信息不一样外,还有一个不同点值得注意。只有2在获得class对象的同时会引起类的初始化,而1和3都不会。还记得获得jdbc连接前注册驱动的操作吗?这就是完成驱动注册的代码
Class.forName("com.mysql.jdbc.Driver");
该方法引起了com.mysql.jdbc.Driver类被加载进内存,同时引起了类的初始化,而注册驱动的逻辑就是在Driver类中的静态代码块中完成的,
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
而通过类.class或classLoad.loadClass()虽然会引起类加载进内存,但不会引起类的初始化。通过下面的例子可以清楚的看到它们之间的区别:
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class ClassInitializeTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<InitialTest> clazz = InitialTest.class;
System.out.println("InitialTest.class:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
Thread.currentThread().getContextClassLoader().
loadClass("com.takumiCX.reflect.InitialTest");
System.out.println("classLoader.loadClass:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
Class.forName("com.takumiCX.reflect.InitialTest");
System.out.println("Class.forName:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
}
}
class InitialTest{
static {
System.out.println("ClassTest 初始化!");
}
}
测试结果如下
4. 运行时反射获取类的结构信息
Class类里的方法比较多,如要是围绕如何获得Method对象,Field对象,Constructor对象,Annotation对象的方法及其重载方法,当然也可以获得类的父类,类实现的接口等信息。
- 代码
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException {
Class<User> clazz = User.class;
//根据构造参数类型获得指定的Constructor对象(包括非公有构造方法)
Constructor<User> constructor = clazz.getDeclaredConstructor(String.class);
System.out.println("获得带String参数的Constructor:"+constructor);
//获得指定字段名的Field对象(包括非公有字段)
Field name = clazz.getDeclaredField("name");
System.out.println("获得字段名为name的Field:"+name);
//根据方法名和方法参数类型获得指定的Method对象(包括非公有方法)
Method method = clazz.getDeclaredMethod("setName", String.class);
System.out.println("获得带String类型参数且方法名为setName的Method:"+method);
//获得类上指定的注解
MyAnnotation myAnnotation = clazz.getAnnotation(MyAnnotation.class);
System.out.println("获得类上MyAnnotation类型的注解:"+myAnnotation);
//获得类的所有实现接口
Class<?>[] interfaces = clazz.getInterfaces();
System.out.println("获得类实现的所有接口:"+interfaces);
//获得包对象
Package apackage = clazz.getPackage();
System.out.println("获得类所在的包:"+apackage);
}
@MyAnnotation
public static class User implements Iuser{
private String name;
public User() {
}
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
static @interface MyAnnotation{
}
static interface Iuser {
}
}
- 测试结果
5. 运行时反射获取泛型的真实类型
除了获得类的常规信息外,类的参数类型(泛型)信息也可以通过反射在运行时获得。泛型类不能用Class来表示,必须借助于反射包下关于类型概念的其他抽象结构。反射包下对类型这个复杂的概念进行了不同层次的抽象,我们有必要知道这种抽象的层次结构以及不同的抽象对应着什么样的类型信息。
5.1 反射包下对类型概念的抽象层次结构
反射包下对类型这个概念进行了不同层级的抽象,它们之间的关系可以用下面这张图表示
- Class:可以表示类,枚举,注解,接口,数组等,但是其不能带泛型参数。
- GenericArrayType:表示带泛型参数的数组类型,如T[ ].
- ParameterizedType:表示带泛型参数的类型,如List< String > ,java.lang.Comparable<? super T>.
- TypeVariable:表示类型变量,比如T,T entends Serializable
- WildcardType:表示通配符类型表达式,比如?,? extends Number等
- Type:关于类型概念的顶级抽象,可以用Type表示所有类型。
5.2 运行时获取带泛型的类,字段,方法参数,方法返回值的真实类型信息
- 代码
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
private Map<String, Integer> map;
public Map<String,Integer> getMap(){
return map;
}
public void setMap(Map<String, Integer> map) {
this.map = map;
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
//获取Class对象
Class<TestGenericType> clazz = TestGenericType.class;
System.out.println("获取类的参数化类型信息:");
//1.获取类的参数化类型信息
Type type = clazz.getGenericSuperclass();//获取带泛型的父类类型
if (type instanceof ParameterizedType) { //判断是否参数化类型
Type[] types = ((ParameterizedType) type).getActualTypeArguments(); //获得参数的实际类型
for (Type type1 : types) {
System.out.println(type1);
}
}
System.out.println("--------------------------");
System.out.println("获取字段上的参数化类型信息:");
//获取字段上的参数化类型信息
Field field = clazz.getDeclaredField("map");
Type type1 = field.getGenericType();
Type[] types = ((ParameterizedType) type1).getActualTypeArguments();
for(Type type2:types){
System.out.println(type2);
}
System.out.println("--------------------------");
System.out.println("获取方法参数的参数化类型信息:");
//获取方法参数的参数化类型信息
Method method = clazz.getDeclaredMethod("setMap",Map.class);
Type[] types1 = method.getGenericParameterTypes();
for(Type type2:types1){
if(type2 instanceof ParameterizedType){
Type[] typeArguments = ((ParameterizedType) type2).getActualTypeArguments();
for(Type type3:typeArguments){
System.out.println(type3);
}
}
}
System.out.println("--------------------------");
System.out.println("获取方法返回值的参数化类型信息:");
//获取方法返回值得参数化类型信息
Method method1 = clazz.getDeclaredMethod("getMap");
Type returnType = method1.getGenericReturnType();
if(returnType instanceof ParameterizedType){
Type[] arguments = ((ParameterizedType) returnType).getActualTypeArguments();
for(Type type2:arguments){
System.out.println(type2);
}
}
}
}
- 结果
5.3 运行时泛型父类获取子类的真实类型信息
- 代码
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType2<T> {
protected Class<T> tClass;
public GenericType2() {
Class<? extends GenericType2> aClass = this.getClass();
Type superclass = aClass.getGenericSuperclass();
if(superclass instanceof ParameterizedType){
Type[] typeArguments = ((ParameterizedType) superclass).getActualTypeArguments();
tClass=(Class<T>) typeArguments[0];
}
}
}
public class TestGenericType2 extends GenericType2<String>{
public static void main(String[] args) {
TestGenericType2 type2 = new TestGenericType2();
System.out.println(type2.tClass);
}
}
- 结果
5.4 泛型的类型信息不是编译期间就擦除了吗
java里的的泛型只在源码阶段存在,编译的时候就会被擦除,声明中的泛型类型信息会变成Object或泛型上界的类型,而使用时都用Object替换,如果要返回泛型类型,则通过强转的方式完成。我们可以写一个泛型类,将其编译成字节码文件后再反编译看看发生了什么。
- 源码
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
private Map<String, Integer> map;
public Map<String,Integer> getMap(){
return map;
}
public void setMap(Map<String, Integer> map) {
this.map = map;
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
}
}
- 反编译后的源码
package com.takumiCX.reflect;
import java.util.ArrayList;
import java.util.Map;
// Referenced classes of package com.takumiCX.reflect:
// GenericType
public class TestGenericType extends GenericType
{
public TestGenericType()
{
}
public Map getMap()
{
return map;
}
public void setMap(Map map)
{
this.map = map;
}
public static void main(String args[])
{
ArrayList list = new ArrayList();
}
private Map map;
}
反编译后的源码泛型信息全部消失了。说明编译器在编译源代码的时候已经把泛型的类型信息擦除。理论上来说,源码中指定的具体的泛型类型,在运行时是无法知道的。但是5.2和5.3的例子里我们确实通过反射在运行时得到了类,字段,方法参数以及方法返回值的泛型类型信息。那么问题出在哪里?关于这个问题我也是纳闷了好久,在网上找了很多资料才得出比较靠谱的答案。泛型如果被用来进行声明,比如说类上,字段上,方法参数和方法返回值上,这些属于类的结构信息其实是会被编译进Class文件中的;而泛型如果被用来使用,常见的方法体中带泛型的局部变量,其类型信息不会被编译进Class文件中。前者因为存在于Class文件中,所以运行时通过反射还是能够获得其类型信息的;而后者因为在Class文件中根本不存在,反射也就无能为力了。
6. 反射创建实例,方法调用,修改字段
- 代码
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class ReflectOpration {
private String name;
public ReflectOpration(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "ReflectOpration{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class<ReflectOpration> clazz = ReflectOpration.class;
//获取带参构造器
Constructor<ReflectOpration> constructor = clazz.getConstructor(String.class);
//反射创建实例,传入构造器参数takumiCX
ReflectOpration instance = constructor.newInstance("takumiCX");
System.out.println(instance);
//根据方法名获取指定方法
Method getName = clazz.getMethod("getName");
//通过反射进行方法调用,传入进行调用的对象作参数,后面可跟上方法参数
String res = (String) getName.invoke(instance);
System.out.println(res);
//获取Field对象
Field field = clazz.getDeclaredField("name");
//修改访问权限
field.setAccessible(true);
//反射修改字段,将名字改为全大写
field.set(instance,"TAKUMICX");
System.out.println(instance);
}
}
- 运行结果
7. 反射的缺点
反射功能强大,使用它我们几乎可以做到java语言层面支持的任何事情。但要注意到这种强大是有代价的。过多的使用反射可能会带来严重的性能问题。曾今作支付平台的系统改造时就碰到过前人滥用反射留下的坑,因为模型对象在web,业务和持久层是不同,但其属性基本一样,所以原来的开发人员为了偷懒大量使用反射来进行这种对象属性的拷贝操作。开发时间是节省了,但给系统性能带来严重的负担,支付接口的调用时间太长,甚至会超时。后来我们将其改回了手动调用setter赋值的方式,虽然工作量不少,但是最后上线的系统性能有了很大的提高,接口调用的响应时间比原来少了近30%。这个例子说明了对反射合理使用的重要性:框架中大量使用反射是因为要提供一套通用的处理流程来减少开发者的工作量,且大部分都在准备或者说容器启动阶段,反射的使用虽然增加了容器启动时间,但因为提高了开发效率,所以是可以接受的;而在对性能有要求的业务代码层面,使用反射会降低业务处理的速度,拖慢接口的响应时间,很多时候是不可接受的。反射一定要在权衡了开发效率和执行性能后,视场景和性能要求谨慎使用。
java基础强化——深入理解反射的更多相关文章
- java基础强化——深入理解java注解(附简单ORM功能实现)
目录 1.什么是注解 2. 注解的结构以及如何在运行时读取注解 2.1 注解的组成 2.2 注解的类层级结构 2.3 如何在运行时获得注解信息 3.几种元注解介绍 3.1 @Retention 3.2 ...
- JAVA基础 (二)反射 深入解析反射机制
在谈论到反射这个问题时,你是否有例如以下疑问? 不管是在.NET还是Java中反射的原理和机制是一样的,理解了一种还有一种就能够迎刃而解,想要理解反射首先须要了解底层的一些概念和执行.理解了反射有助于 ...
- Java基础之深入理解Class对象与反射机制
深入理解Class对象 RRIT及Class对象的概念 RRIT(Run-Time Type Identification)运行时类型识别.在<Thinking in Java>一书第十四 ...
- java基础篇3之反射
1.反射的基础 反射的基石---->Class类 java程序中的各个java类属于同一类事物,描述这类事物的java类名就是Class 获取字节码对应的实例对象(Class类型) class ...
- Java基础系列-深入理解==和equals的区别(一)
一.前言 说到==和equals的问题,面试的时候可能经常被问题到,有时候如果你真的没有搞清楚里边的原因,被面试官一顿绕就懵了,所以今天我们也来彻底了解一下这个知识点. 二.==和equals的作用 ...
- java基础(32):类加载、反射
1. 类加载器 1.1 类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化. 加载 就是指将class文件读入内存,并为之创建一个C ...
- Java基础(十一)——反射
一.概述 1.介绍 Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法. 加载完类 ...
- 【Java基础】RTTI与反射之Java
一.引言 很多时候我们的程序可能需要在运行时识别对象和类的信息,比如多态就是基于运行时环境进行动态判断实际引用的对象.在运行时识别对象和类的信息主要有两种方式:1.RTTI,具体是Class对象,它假 ...
- JAVA基础知识之JVM-——使用反射生成并操作对象
Class对象可以获取类里的方法,由Method对象表示,调用Method的invoke可以执行对应的方法:可以获取构造器,由Constructor对象表示,调用Constructor对象的newIn ...
随机推荐
- fatal: The remote end hung up unexpectedly解决办法
$ git config --global http.postBuffer 2428000 git config http.postBuffer 524288000 配置完成后 git pull一下, ...
- 谷歌被墙后,能够搜索的ip地址
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/langresser/article/details/32339707 http://209.116. ...
- 在一个form中有两个submit,值分别为修改和删除,如何在提交时用js判断submit值为修改还是删除呢
同一个form里,不管哪个 submit 都是直接提交form表单里的内容. 要达到你的目的,就不能用类型为 submit 的按钮,要用 button,然后加onclick 方法来自定义预处理参数,然 ...
- [LeetCode系列]BST有效性确定问题[前序遍历]
给定一个BST的根节点, 试判断此BST是否为符合规则的BST? 规则: 对于一个BST的节点, 它左侧的所有节点(包括子节点)必须小于它本身; 它右侧的所有节点(包括子节点)必须大于它本身; 它的左 ...
- Shell脚本一键安装LNMP环境
https://sourceforge.net/projects/opensourcefile/files/ Nginx是一款高性能的HTTP和反向代理服务器.Nginx在反向代理,Rewrite规则 ...
- eclipse导出jar,再转换为exe可执行程序
转自: https://blog.csdn.net/mommomm/article/details/8227876 若只想知道如何把jar转换成exe,直接看第四步即可. 一.导出jar文件: 选中你 ...
- IMAP简单研究
IMAP的相关详细介绍: http://www.imapwiki.org/ClientImplementationhttp://tools.ietf.org/html/rfc3501 1.连接服务器 ...
- linux下踢出已登录用户
通过xshell登录到linux,看到如下所示,有3个用户,但是前面两个不知在哪登录的了,那就踢出吧. 先确认一下自己是哪个 顺便注意一下“whoami”和“who am i”的不同 然后踢出前面两个 ...
- 使用Java读取配置文件
实现起来,相对比较简单,留个备案吧,废话也不多说,请看代码: package com.jd.***.config; import org.junit.*; import java.io.IOExcep ...
- orzdba_monitor.sh和orzdba
1.脚本 #!/bin/bash # line: V1.0 # mail: gczheng@139.com # data: 2018-04-23 # script_name: orzdba_monit ...