Java进阶04-动态代理、类加载

动态代理和类加载

本节主要复习动态代理和类加载机制。这2个知识点是非常重要的,也是很常见的,可能我们自己用的并不多,但是很多框架中的基础都是它们2个。如果不知道这些知识 那么看那些开源框架的源码就会很吃力。是谓 基础不牢地动山摇
在这里插入图片描述

类加载

还是按标准的灵魂5问来学习:

  • 什么是类加载
  • 怎么使用类加载
  • 类加载的优缺点
  • 类加载的原理
  • 类加载的使用场景

什么是类加载
类加载是一种机制是一套流程和动作。我们写的源码其实就是文本文件,如我们写的第一个java程序 HelloWorld.java 。这个就是源文件给人看的。
在这里插入图片描述
但是这个文件并不能直接运行, 需要编译成HelloWorld.class 这个是字节码文件 打开里面其实是二进制,但是一般不会显示0101这种而是会以16进制显示。这个给java虚拟机看的。
在这里插入图片描述
你要看到话需要对着-java虚拟机规范表才知道代表什么意思。这个也是不能直接在电脑上运行的。它只能运行在JVM中,怎么运行,当我们执行java HelloWorld
在这里插入图片描述
这里java 就代表运行虚拟机,后面的HelloWorld就是 我们的字节码,意思就是运行虚拟机 然后在虚拟机上运行HelloWorld 字节码。虚拟机会把字节码 翻译成对应操作系统能识别的机器码就可以在电脑上运行了。
类加载就是:虚拟机通过全限定名(就是路径)把字节码文件读取进内存,怎么读?肯定是IO啊,然后进行必要的验证、准备、解析、初始化等工作 最后转为规定的数据结构存储在内存中方法区,并且生成一个Class对象来描述这个类文件,后续通过这个Class就可以访问到这个类文件。

怎么使用类加载
首先需要搞清楚 类加载器有哪些,并不是所有的 类都是又一种类加载器 去加载的,比如JDK自带的类,我们自己写的类。根据官方文档类加载器有3种:

  1. BootStrap ClassLoder —BootstrapClassLoader引导类加载器,这个是最顶层的类加载器,它主要加载核心类库,JRE中的库 具体就是%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。这里提一句JDK:java develop kit 就是开发工具包。 JRE:Java Runtime Enviroment是指Java的运行环境。
    在这里插入图片描述

  2. Extention ClassLoader --ExtClassLoader扩展类加载器,加载我们开发用的到基础库,不懂的可以点进去看看有哪些类具体目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
    在这里插入图片描述

  3. Appclass ClassLoader—AppClassLoader也称为SystemAppClass 加载当前应用的classpath的所有类。就是你自己写的类。
    在这里插入图片描述
    BootstrapClassLoader这个类在虚拟机中是C++编写的。不过在Launcher中也有一个BootClassPathHolder 私有的静态内部类
    AppClassLoader和ExtClassLoader 是java编写的在 sun.misc包下 其实是Launcher中的2个静态内部类。
    这3个类我们都改了不了什么,系统的东西。
    不过我们可以自定义自己的类加载器 只要继承ClassLoader就行了。举个栗子:
    步骤也很简单:
    1.写一个类继承自ClassLoader抽象类。
    2.复写它的findClass()方法。
    3.在findClass()方法中调用defineClass()。
    如:

package com.zh.loader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * @descriable 自定义苹果 类 加载器
 */
public class AppleLoader extends ClassLoader {
	private static String TAG = "AppleLoader";
	/**就是.class文件的路径 如果是放在D盘 则 D:/*/
	private String mDir;
	public AppleLoader(String dir) {
		MainLoader.println(TAG, dir);
		this.mDir = dir;
	}
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		//这个name是全限定名 什么意思?
		//其实就是包名加类名。如本类的全限定名就是 com.zh.loader.AppleLoader.class。
		//本质其实还是路径  因为你编译后的路径会变成 磁盘路径+com/zh/loader/AppleLoader.class
		MainLoader.println(TAG, name);
		byte[] data = new byte[1024];
		InputStream inputStream;
		ByteArrayOutputStream byteArrayOutputStream;
		try {
			File file = new File(mDir, getFileName(name));
			inputStream = new FileInputStream(file);
			byteArrayOutputStream = new ByteArrayOutputStream();
			int len = 0;
			while ((len = inputStream.read(data)) != -1) {
				MainLoader.println(TAG, len);
				byteArrayOutputStream.write(data, 0, len);
			}
			//这部分就是把Apple.class文件从磁盘读取出来 
			byte[] buff = byteArrayOutputStream.toByteArray();
			inputStream.close();
			byteArrayOutputStream.close();
			MainLoader.println(TAG, buff.length);
			//defineClass是原生方法:可以把符合虚拟机规则的 byte[]数组 转化成为描述类文件的Class对象
			return defineClass(name, buff, 0, buff.length);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return super.findClass(name);
	}
	// 获取要加载 的class文件名
	private String getFileName(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return name + ".class";
		} else {
			return name.substring(index + 1) + ".class";
		}
	}
}

上面的注释已经很清楚了,关键点就是全限定名的理解。加载了类有什么用?什么都看不见啊 所以还需要进行测试:
先编写一个Apple.java 编译成Apple.class,然后把Apple.class放到电脑E盘根目录下(随便你放到哪)。

package com.zh.loader;

public class Apple {
	public void describe() {
		System.out.println("我是苹果");
	}
}

这样我们就从APK外部加载了一个类进我们的虚拟机,如何用这个类?只能通过反射如:

public static void main(String[] args) {
		try {
			println(TAG, "测试自定义类加载");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
			Method[] declaredMethods = aClass.getDeclaredMethods();
			declaredMethods[0].invoke(aClass.newInstance());

		} catch (Exception e) {
			println(TAG, e);
		}
	}

运行结果:
在这里插入图片描述
在这里插入图片描述
可以看到我们的工程中没Apple类,但是却可以使用。这个Apple类我们可以放到本地,也可以放到网络上,用的时候再 下载过来。这个就是热修复和插件化的基础。还可以动态替换掉类。

类加载的优缺点
优点:

  1. 动态的替换和增加额外的类。
  2. 热修复热更新插件化

缺点:

  1. 需要反射使用很慢
  2. 非常规手段有风险

类加载的原理
原理就是多啦,涉及了虚拟机的类加载机制。主要的源码就是ClassLoader中的loadClass()方法。它涉及双亲加载机制。源码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看到它是通过递归的方式去加载类,这就是双亲加载机制。
具体解释
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
resolveClass:就是用来连接的。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java规范中的Execution描述进行链接。等下我们来研究这个resolveClass()方法什么时候才会执行。
比如我们自定义的这个类加载器:
resolve一直都是会是false;所以其实我们调用loadClass时,并不会出发类的初始化。可以验证如下:
我们修改一下Apple类 增加一段静态代码块,

package com.zh.loader;

public class Apple {

	static{
		System.out.println("我被初始化了");
	}

	public void describe() {
		System.out.println("我是苹果");
	}
}

测试,先注释 反射调用相关,就只加载

public static void main(String[] args) {
		try {
			println(TAG, "测试自定义类加载");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
//			Method[] declaredMethods = aClass.getDeclaredMethods();
//			declaredMethods[0].invoke(aClass.newInstance());
		} catch (Exception e) {
			println(TAG, e);
		}
	}

运行结果:
在这里插入图片描述
接着加上反射调用方法:

public static void main(String[] args) {
		try {
			println(TAG, "测试自定义类加载");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
			Method[] declaredMethods = aClass.getDeclaredMethods();
			declaredMethods[0].invoke(aClass.newInstance());
		} catch (Exception e) {
			println(TAG, e);
		}
	}

运行结果:
在这里插入图片描述
类的加载 和类的初始化
这里要搞清楚,类的加载和类的初始化是2个不同的东西。加载只是把类文件读取进内存并生成一个Class对象。初始化 才是运行类中的代码。类中的静态代码。

什么情况下需要开始类加载过程的第一个阶段:“加载”。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。
1.遇到new,getstatic,putstatic,invokestatic这4条指令;
2.初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类(注意!如果其父类是接口的话,则不要求初始化父类);
3.访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
4.使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
5.虚拟机启动时,定义了main()方法的那个类先初始化

以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用”
接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

以下情况会触发类的初始化:

1.遇到new,getstatic,putstatic,invokestatic这4条指令;
2.使用java.lang.reflect包的方法对类进行反射调用;
3.初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类(注意!如果其父类是接口的话,则不要求初始化父类);

以下情况不会触发类的初始化:
1同类子类引用父类的静态字段,不会导致子类初始化。至于是否会触发子类的加载和验证,取决于虚拟机的具体实现;
2通过数组定义来引用类,也不会触发类的初始化;例如:People[] ps = new People[100];
3.引用一个类的常量也不会触发类的初始化

双亲委派:
前面说过,系统的类加载器有3种,具体的关系:
BootstrapClassLoader(祖父)–>ExtClassLoader(爷爷)–>AppClassLoader(也称为SystemClassLoader)(爸爸)–>自定义类加载器(儿子)
彼此相邻的两个为父子关系,前为父,后为子。
比如现在我们写了一个类Apple,整个加载过程如下:
在这里插入图片描述
简单描述一下:
1
一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
2
递归,重复第1部的操作。
3
如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是
sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
4
Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
5
ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

双亲委托模型好处
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达更通俗一些:比较两个类是否”相等”,只有再这两个类是有同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

类隔离机制
同一个类Dog可以加载两次(只要loader1和loader3不是父子关系即可,加载出的 Class 对象不同),不同运行空间内的类不能互相访问(eg. loader1和loader3不是父子关系,则Loader1加载的Dog不能访问lodaer3加载的Sample)
父类加载器无法访问到子类加载器加载的类,除非使用反射。Eg. Loader1 的父加载器是 系统类加载器,假设 Sample 类由 loader1 加载, 使用 loader1 的类 Test 是由系统类加载器加载的,例如下面这段代码属于 Test 类,那么如果直接使用注释部分的代码(即通过常规的方式使用 Sample 是不行的),必须通过反射。

类加载的使用情景
热修复,插件化的开源框架使用的比较多。我们平时开发基本不用。但是如果你不理解的 那么插件化等框架就用不好。

其实具体可以去看研究《深入理解Java虚拟机》第二版。讲得非常详细,同时也很比较难理解需要多看几遍。
在这里插入图片描述

动态代理

  • 什么是动态代理
  • 怎么使用动态代理
  • 动态代理的优缺点
  • 类加载的动态代理
  • 动态代理的使用场景

什么是动态代理
代理生活中常见的就是 被告-律师-法官。法官提问题,被告回答一点,然后就是律师去回答。
代码中的代理也类似。有一种模式就叫代理模式。
在这里插入图片描述
动态代理就是不需要提前写代理类的代理模式。
怎么使用动态代理
这个也简单,和静态代理类似有个基本套路在、举个例子:现在有个接口描述水果。

public interface IFruit {
	void descriabe();
}
public class Apple implements IFruit{

	@Override
	public void descriabe() {
		System.out.println("我是苹果!!!");
	}
	
}

如果是静态代理的话:

public class AppleProxy implements IFruit {
	private IFruit mFruit;

	public AppleProxy(IFruit mFruit) {
		this.mFruit = mFruit;
	}

	@Override
	public void descriabe() {
		System.out.println("吃的");
		mFruit.descriabe();
		System.out.println("甜的");
	}

}

测试:

	public static void main(String[] args) {
		IFruit fruit = new AppleProxy(new Apple());
		fruit.descriabe();
	}

运行结果:
在这里插入图片描述
这个比较简单。那怎么处理不写AppleProxy也可以增强desciable();
答案就是动态代理。其实java提供好了一套 动态代理API流程如下:
定义一个类实现InvocationHandler接口

public class AppleDynamicProxy implements InvocationHandler {

	private Object object;

	public AppleDynamicProxy(Object object) {
		this.object = object;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		System.out.println("吃的");
		method.invoke(object, args);
		System.out.println("甜的");
		return null;
	}

}

然后测试

	public static void main(String[] args) {

		Apple apple = new Apple();

		InvocationHandler invocationHandler = new AppleDynamicProxy(apple);

		Class<?> class1 = Apple.class;

		IFruit fruit = (IFruit) Proxy.newProxyInstance(class1.getClassLoader(),
				class1.getInterfaces(), invocationHandler);

		fruit.descriabe();

	}

运行结果:
在这里插入图片描述
可以看到我们的AppleDynamicProxy并没有继承IFruit接口,但是收到describe()方法,并且在它调用前后进行了增强。
动态代理的优缺点
优点:不修改被代理对象的源码上,进行功能的增强。
缺点:暂无-可能用到反射会比较慢
动态代理的原理
主要用到了反射,如果对反射不熟悉的,看起来可能比较吃力。

动态代理的使用场景
这取决于你自己想干什么。主要看业务的需求。

在这里插入图片描述
关键就这个newProxyInstance通过名字就知道 创建一个代理类的实例。getProxyClass0会根据你传入的类加载器和接口 去生成一个代理类。
总结
代理分为静态代理和动态代理两种。
静态代理,代理类需要自己编写代码写成。
动态代理,代理类通过 Proxy.newInstance() 方法生成。
不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。
静态代理和动态代理的区别是在于要不要开发者自己定义 Proxy 类。
动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。
代理模式本质上的目的是为了增强现有代码的功能。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值