java类加载: SPI机制及反射原理
java SPI、反射原理:是干什么的?解决了什么问题?
SPI介绍(待更新):
SPI:(Service Provider Interface, 是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,则只需要给它添加个实现,比如经常遇到的就是java.sql.Driver接口,)
主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对统一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
当服务提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的是实现的工具类是:java.util.ServiceLoader。
SPI & API:(当接口属于调用方,即我们自己定义的代码时,我们就将其称为SPI,全称为Service Provider Interface,SPI的规则如下)
1.概念上更依赖调用方
2.组织上位于调用方所在的包中
3.实现位于独立的包中(也可以认为在提供方中)
SPI的用途:(做什么的)
服务提供发现机制;
完成指定模块的查找(加载类)、实例化
SPI解决了什么问题:
使用特定的接口实现时,不用修改现有的代码,只是通过一个简单的配置就可以达到效果。(如DriverManager的作用:传统做法需要使用Class.forName("**.Driver")来完成指定驱动实现类的加载,现在只需要在/resource/META-INF/service/全限定文件 中指定要使用的指定驱动实现类的类名即可完成类的加载)
JDK SPI需要遵循的规范:
需要一个目录:
META/service
放到ClassPath下面
目录下面放置一个配置文件:
文件名是要扩展的接口全名
文件内部为要实现的接口实现类
文件必须为UTF-8编码
如何使用:
ServiceLoad.load(xx.class) (xx.class为接口名称,如下:)
ServiceLoad<HelloInterface> loads = ServiceLoad.load(HelloInterface.class)
总结:
线程上下文类加载器:当高层提供了统一接口让底层去实现,同时又要是在高层加载(或实例化)底层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载其代为托管。
getContextClassLoader()和setContextClassLoader(ClassLoader cl)分别用来获取和设置线程上下文类加载器,如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行的初始线程的上下文类加载器是系统类加载器。
loadInitialDrivers() -> ServiceLoader.load() -> getContextClassloader() -> load(Driver.class, getContextClassloader()) -> ServiceLoader(Driver.class, getContextClassloader()) -> reload() ->
反射原理
Class类型信息:(Java中使用类java.lang.Class来指向一个类型信息,获取一个Class对象的方法)
类名.class (如Object.class、Integer.class等)
getClass方法:(Object类有一个方法 public final native Class<?> getClass();如:Integer integer = new Integer(12); integer.getClass();)
forName方法:()
反射字段属性:Class中有关获取字段属性的方法有:
public Field[] getFields(): 返回该类型的所有public修饰的属性,包括父类的
public Field[] getFields(String name): 根据字段名称返回相应的字段
...
(如:
Class<People> cls = People.class;
Field name = cls.getField("name");
People people = new People();
name.set(people, "hello");
System.out.println(people.name);)
反射方法:
public Method[] getMethods(): 返回所有的public方法,包括父类中的
public Method[] getMethods(String name, Class<?>...parameterTypes): 返回指定的方法
...
(如:
Class<People> cls = People.class;
Method syaHello = cls.getMethod("sayHello");
People people = new People();
sayHello.invoke(people);)
反射构造器:
public Constructor<?> [] getConstructors(): 返回所有public 修饰的构造器
public Constructor<?> [] getDeclaredConstructors(): 返回所有的构造器,无视访问修饰符
...
反射与数组、泛型:
public native Class<?> getComponentType();
public static Object newInstance(Class<?> componentType, int length)
...
反射是干什么的:
能够在程序运行时动态的创建对象等操作;
反射解决了什么问题:(使用场景: 如果一个程序对象是自己new的,程序相当于写死了给jvm去跑。假如一个服务器上突然遇到某个请求需要用到某个类,但是没有加载进jvm,那是不是要停下来自己写段代码,new一下,再启动服务器。 显然事情不应该是这个样子的)
当程序在运行时,需要动态的加载一些类,这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载。
<font color="#660000">反射原理:</font>
java程序是怎么运行的:JVM启动后,代码就会被编译成.class文件,然后被类加载器加载进JVM内存中,在JVM内存中,方法区存放的是代码中所有的类的信息;堆中存放的是方法区中的类对应的Class对象,这样在代码执行的过程中,当遇到需要新建类对象的时候,就通过Class对象新建该类对应的对象。(Java栈、本地栈用来存放对象中的变量、运算符以及静态变量方法等数据)
那么反射主要是负责程序运行时动态的加载类与创建对象,所以在java程序运行的基础上,通过classForName()等方法就可以动态加载类与创建对象了。
问题思考:
为什么要使用SPI,Class.forName("com.mysql.jdbc.Driver")来加载驱动不好吗?(即其好处是什么)
相关拓展
双亲委托模型:
ClassLoader使用的是双亲委托模型来搜索类,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图在亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下一次检查的,首先由最顶层的类加载器BootStrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader进行加载,如果也没加载到,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的Class实例对象。
JVM在搜索类的时候,是如何判定两个class是相同的:
不仅要判断两个类名是否相同,而且也要判断是否是同一个类加载器实例加载的。
ClassLoader的体系架构:
1.自底向上检查类是否已经加载
2.自顶向下尝试加载类