深入解析Java类加载的案例与实战教程

目录
  • 一、Tomcat类加载器架构
  • 二、动态代理的原理
  • 三、Java语法糖的改变

本篇文章主要介绍Tomcat类加载器架构,以及基于类加载和字节码相关知识,去分析动态代理的原理。

一、Tomcat类加载器架构

Tomcat有自己定义的类加载器,因为一个功能健全的Web服务器,都要解决 如下的这些问题:

  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的 需求。两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求每个类库在一个服务 器中只能有一份。
  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器 上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒 不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟 机的方法区就会很容易出现过度膨胀的风险。
  • 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。基于安 全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
  • 支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能。我们知道JSP文件最终要被编译 成Java的Class文件才能被虚拟机执行,所谓的hotswap,就是使用新的代码替换掉已经加载的这个Class中的内容。

由于存在上述问题,在部署Web应用时,单独的一个ClassPath就不能满足需求了,所以各种Web服 务器都不约而同地提供了好几个有着不同含义的ClassPath路径供用户存放第三方类库,这些路径一般 会以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常每一 个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库

在Tomcat目录结构中,把Java类库放置在这4组目录中,每一组都有独立的含义,分别是:

  • 放置在/common目录中。类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见。放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器, 这些类加载器按照经典的双亲委派模型来实现,关系如下图所示。

灰色背景的3个类加载器是默认提供的类加载器,而JDKCommon类加载器、Catalina类加载器(也称为Server类 加载器)、Shared类加载器和Webapp类加载器则是Tomcat自己定义的类加载器。

它们分别加 载/common/、/server/、/shared/*和/WebApp/WEB-INF/*中的Java类库。
其中WebApp类加载器和JSP类加载器通常还会存在多个实例,每一个Web应用程序对应一个WebApp类加载器每一个JSP文件对应 一个JasperLoader类加载器

由上图得知:

  • Common类加载器能加载的类都可以被Catalina类加载器和Shared 类加载器使用
  • 而Catalina类加载器和Shared类加载器自己能加载的类则与对方相互隔离
  • WebApp类 加载器可以使用Shared类加载器加载到的类,但各个WebApp类加载器实例之间相互隔离。
  • JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件,它存在的目的就是为了被 丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新 的JSP类加载器来实现JSP文件的HotSwap功能。

本例中的类加载结构在Tomcat 6以前是它默认的类加载器结构,在Tomcat 6及之后的版本简化了默 认的目录结构,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会 真正建立Catalina类加载器和Shared类加载器的实例,否则会用到这两个类加载器的地方都会用 Common类加载器的实例代替。

Tomcat 6之后也 顺理成章地把/common、/server和/shared这3个目录默认合并到一起变成1个/lib目录,这个目录里的类库 相当于以前/common目录中类库的作用。

那么笔者不妨再提一个问题让各位读者思考一下:前 面曾经提到过一个场景,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring 放到Common或Shared目录下让这些程序共享Spring要对用户程序的类进行管理,自然要能访问到用 户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的。那么被Common类加载器或 Shared类加载器加载的Spring如何访问并不在其加载范围内的用户程序呢?

答案:如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载的bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。

二、动态代理的原理

“字节码生成”并不是什么高深的技术,因为JDK里面的Javac命令就是字节码生成技术的“老祖 宗”,并且Javac也是一个由Java语言写成的程序。

在Java世界里面除了Javac和字 节码类库外,使用到字节码生成的例子比比皆是,如Web服务器中的JSP编译器,编译时织入的AOP框 架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提 高执行速度。我们选择其中相对简单的动态代理技术来讲解字节码生成技术是如何影响程序运作的。

什么是动态代理?

动态代理中所说的“动态”,是指实 现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系 后,就可以很灵活地重用于不同的应用场景之中。

下面代码演示了一个最简单的动态代理的用法,原始的代码逻辑是打印一句“hello world”,代 理类的逻辑是在原始类方法执行前打印一句“welcome”。我们先看一下代码,然后再分析JDK是如何做 到的。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
    interface IHello {
        void sayHello();
    }
    static class Hello implements IHello {
        @Override
        public void sayHello() {
            System.out.println("hello world");
        }
    }
    static class DynamicProxy implements InvocationHandler {
        Object originalObj;
        Object bind(Object originalObj) {
            this.originalObj = originalObj;
            return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("welcome");
            return method.invoke(originalObj, args);
        }
    }
    public static void main(String[] args) {
        IHello hello = (IHello) new DynamicProxy().bind(new Hello());
        hello.sayHello();
    }
}

运行结果如下:

在上述代码里,唯一的“黑匣子”就是Proxy::newProxyInstance()方法,除此之外再没有任何特殊之 处。这个方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象。

newProxyInstance一共传进去三个参数:

  • loader第一个参数,代表的是被代理类的类加载器
  • interfaces代理类要实现的被代理类接口
  • InvocationHandler代表的是将方法调用分派给的调用处理程序

跟踪这个方法的 源码,可以看到程序进行过验证、优化、缓存、同步、生成字节码、显式类加载等操作,前面的步骤 并不是我们关注的重点,这里只分析它最后调用sun.misc.ProxyGenerator::generateProxyClass()方法来完 成生成字节码的动作

这个方法会在运行时产生一个描述代理类的字节码byte[]数组。如果想看一看这 个在运行时产生的代理类中写了些什么,可以在main()方法中加入下面这句:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

执行完 可以用idea在debug状态下直接双击shift搜索$Proxy即可找到java文件,如下:

import com.gzl.cn.DynamicProxyTest.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
final class $Proxy0 extends Proxy implements IHello {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
	// 此处由于版面原因,省略equals()、hashCode()、toString()3个方法的代码

    public final void sayHello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.gzl.cn.DynamicProxyTest$IHello").getMethod("sayHello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

动态代理的原理:

  • 通过ProxyGenerator::generateProxyClass()生成一个代理类
  • 这个代理类的实现代码也很简单,它为传入接口中的每一个方法,以及从java.lang.Object中继承来 的equals()、hashCode()、toString()方法都生成了对应的实现,并且统一调用了InvocationHandler对象的 invoke()方法来实现这些方法的 内容。
  • 代码中的“super.h”就是父类Proxy中保存的InvocationHandler实例变量,而实例变量就是刚刚传入的new Hello()。
  • 所以无论调用动态代理的哪一 个方法,实际上都是在执行InvocationHandler::invoke()中的代理逻辑。

这个例子中并没有讲到generateProxyClass()方法具体是如何产生代理类“$Proxy0.class”的字节码 的,大致的生成过程其实就是根据Class文件的格式规范去拼装字节码,但是在实际开发中,以字节为 单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码。

对于用 户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适。如果 读者对动态代理的字节码拼装过程确实很感兴趣,可以在OpenJDK的 java.base\share\classes\java\lang\reflect目录下找到sun.misc.ProxyGenerator的源码。

三、Java语法糖的改变

在Java世界里,每一次JDK大版本的发布,对Java程 序编写习惯改变最大的,肯定是那些对Java语法做出重大改变的版本。

  • 譬如JDK 5时加入的自动装箱、 泛型、动态注解、枚举、变长参数、遍历循环(foreach循环);譬如JDK 8时加入的Lambda表达式、 Stream API、接口默认方法等。
  • 事实上在没有这些语法特性的年代,Java程序也照样能写。 现在问题来了,如何把高版本JDK中编写的代码放到低版本JDK 环境中去部署使用?

为了解决这个问题,一种名为“Java逆向移植”的工具(Java Backporting Tools)应 运而生,Retrotranslator和Retrolambda是这类工具中的杰出代表。

Retrotranslator的作用是将JDK 5编译出来的Class文件转变为可以在JDK 1.4或1.3上部署的版本, 它能很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性, 甚至还可以支持JDK 5中新增的集合改进、并发包及对泛型、注解等的反射操作。

Retrolambda的作 用与Retrotranslator是类似的,目标是将JDK 8的Lambda表达式和try-resources语法转变为可以在JDK 5、JDK 6、JDK 7中使用的形式,同时也对接口默认方法提供了有限度的支持。

什么是语法糖?

在前端编译器层面做的改进。这种改进被称作语法糖。也就是这些语法糖主要是帮助我们这些开发人员减少代码量,但是并没有省略掉,只是交给了javac编译器,来替我们做了转换。

  • 如自动装箱拆箱,实际上就是Javac编 译器在程序中使用到包装对象的地方自动插入了很多Integer.valueOf()、Float.valueOf()之类的代码
  • 使用enum关键字定义常量,尽管从 Java语法上看起来与使用class关键字定义类、使用interface关键字定义接口是同一层次的,但实际上这 是由Javac编译器做出来的假象,从字节码的角度来看,枚举仅仅是一个继承于java.lang.Enum、自动生 成了values()和valueOf()方法的普通Java类而已。

到此这篇关于深入解析Java类加载的案例与实战的文章就介绍到这了,更多相关Java类加载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Java父子类加载顺序

    先上桌结论,优先被加载的顺序如下: 父类静态成员变量 > 父类静态代码块 > 子类静态成员变量 > 子类静态代码块 > 父类非静态成员变量 > 父类非静态代码块 > 父类构造方法 > 子类非静态成员变量 > 子类非静态代码块 > 子类构造方法 这么长怎么记呀?! 这里帮大家小结几个特点: 成员变量 > 代码块 > 构造方法(构造器). 静态(共有) > 非静态(私有). 子类静态 > 父类非静态(私有). 1. 静态 >

  • Java虚拟机之对象创建过程与类加载机制及双亲委派模型

    目录 一.对象的创建过程: 1.对象的创建过程: 2.对象的访问方式: 二.类加载机制: 2.1.加载阶段: 2.2.验证阶段: 2.3.准备阶段: 2.4.解析阶段: 2.5.初始化: 2.5.1.类的主动引用: 2.5.2.类的被动引用: 2.5.3.()方法的特点: 三.类加载器与双亲委派模型: 3.1.JVM 的类加载器: 3.2.双亲委派模型: 3.2.1.双亲委派模型的工作原理: 3.2.2.双亲委派模型的优点: 3.3.类加载器源码:loadClass() 3.4.如何破坏双亲委派

  • Java类加载的过程详解

    目录 Java:类加载过程 1.加载--3件事 2.连接 2.1.验证 2.2.准备 2.3.解析 3.初始化 总结 Java:类加载过程 1.加载--3件事 1.通过全类名获取定义此类的二进制字节流(eg:从jar.war中获取): 2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构: 3.在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口. 2.连接 加载阶段和连接阶段的部分内容是交叉进行的,加载尚未结束,连接阶段可能就开始运行了. 2.1.验证 2.2.准备

  • java虚拟机原理:类加载过程详解

    目录 一.Java 类加载过程 1.字节码编译 2.加载 3.连接 4.初始化 总结 一.Java 类加载过程 1.字节码编译 编写好 Java 源码 Student.java , 使用 javac 将上述 Java 源码编译成 Class 字节码文件 Student.class , 2.加载 加载 : 通过 " 类加载子系统 " 将该字节码文件 , 加载到 Java 虚拟机内存中 的 方法区 , 然后开始执行 " 连接 " 操作 , 类加载时机 : Java 程序

  • 深入探究Java中的类加载机制

    目录 前言 步入正题 类的加载过程: 1.加载 2.验证 3.准备 4.解析 5.初始化 类加载器 源码 总结 前言 学生时代应抱着问题去学习一门语言,例如:在学习java语言的过程中,我遇到过java主方法main里面参数到底是存的什么?还有java语言的Object是如何成为所有类的父类的?java虚拟机到底如何解析字节码文件的?java是纯面向对象编程语言那么它的类是怎样的加载过程?今天我就带着大家一探究竟. 步入正题 首先我们都熟悉DOS界面去执行我们编写的源码,第一步使用javac x

  • java虚拟机JVM类加载机制原理(面试必问)

    目录 1.类加载的过程. 1)加载 2)验证 3)准备 4)解析 5)初始化 2.Java 虚拟机中有哪些类加载器? 1)启动类加载器(Bootstrap ClassLoader): 2)扩展类加载器(Extension ClassLoader): 3)应用程序类加载器(Application ClassLoader): 3.什么是双亲委派模型? 4.为什么使用双亲委派模式? 5.有哪些场景破坏了双亲委派模型? 1)线程上下文类加载器 2)Tomcat 的多 Web 应用程序 3)OSGI 实现

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

  • 深入解析Java类加载的案例与实战教程

    目录 一.Tomcat类加载器架构 二.动态代理的原理 三.Java语法糖的改变 本篇文章主要介绍Tomcat类加载器架构,以及基于类加载和字节码相关知识,去分析动态代理的原理. 一.Tomcat类加载器架构 Tomcat有自己定义的类加载器,因为一个功能健全的Web服务器,都要解决 如下的这些问题: 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离.这是最基本的 需求.两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求每个类库在一个服务 器中只能有一份

  • Java后台接口开发初步实战教程

    上图是查询列表的接口,get方式 上图是用户注册的接口,同样是get,post方式也很简单 开发工具:IntelliJ IDEA 2016.3.5 ORM框架:MyBatis 数据库:MySql 服务器:tomcat7.0 公司使用的的orm框架是Hibernate,使用起来感觉比mybatis好用多了,毕竟经过了公司这么多项目的考验,总比自己用mybatis写的项目可靠,但以下分享的还是mybatis的代码 注册接口方法:http://192.168.1.116:8080/register?u

  • 通过面试题解析 Java 类加载机制

    在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.ou

  • 源码解析Java类加载器

    参考内容: 深入理解Java虚拟机(JVM高级特性与最佳实践) --周志明老师 尚硅谷深入理解JVM教学视频--宋红康老师 我们都知道Java的类加载器结构为下图所示(JDK8及之前,JDK9进行了模块化): 关于三层类加载器.双亲委派机制,本文不再板书,读者可自行百度. 那么在JDK的源码中,三层结构的具体实现是怎么样的呢? Bootstrap ClassLoader(引导类加载器) 引导类加载器是由C++实现的,并非Java代码实现,所以在Java代码中是无法获取到该类加载器的. 一般大家都

  • Java之类加载机制案例讲解

    1.类加载 <1>.父子类执行的顺序 1.父类的静态变量和静态代码块(书写顺序) 2.子类的静态变量和静态代码块(书写顺序) 3.父类的实例代码块(书写顺序) 4.父类的成员变量和构造方法 5.子类的实例代码块 6.子类的成员变量和构造方法 <2>类加载的时机 如果类没有进行初始化,则需要先进行初始化,虚拟机规范则是严格规定有且只有5种情况必须先对类进行初始化(而加载,验证,准备要在这个之前开始) 1.创建类的实例(new的方式),访问某个类的静态变量,或者对该静态变量赋值,调用类

  • Java类加载器ClassLoader用法解析

    这篇文章主要介绍了Java类加载器ClassLoader用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 正文 当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.连接.初始化3个步骤来对该类进行初始化.如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化. 一.类加载过程 1.加载 加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说

  • java类加载机制、类加载器、自定义类加载器的案例

    类加载机制 java类从被加载到JVM到卸载出JVM,整个生命周期包括:加载(Loading).验证(Verification).准备(Preparation).解析(Resolution).初始化(Initialization).使用(using).和卸载(Unloading)七个阶段. 其中验证.准备和解析三个部分统称为连接(Linking). 1.加载 加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Clas

  • Java方法重载的使用实战案例

    本文实例讲述了Java方法重载的使用.分享给大家供大家参考,具体如下: 一 重载方法valueOf的使用实战 1 代码 import java.lang.String ; public class OverloadValueOf { public static void main(String args[]){ byte num_byte = 12; short num_short = 34; int num_int = 12345; float num_float = 12.34f; bool

  • Java中内部类使用方法实战案例分析

    本文实例讲述了Java中内部类使用方法.分享给大家供大家参考,具体如下: 一 点睛 分三种情况讨论内部类的使用: 1 在外部类内部使用内部类 2 在外部类以外使用非静态内部类 3 在外部类以外使用静态内部类 二 在外部类内部使用内部类 可以直接通过内部类的类名来定义变量,通过new调用内部构造器来创建实例. 不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员. 三 在外部类以外使用非静态内部类 1 点睛 private 修饰的内部类只能在外部类

  • Java SPI用法案例详解

    1.什么是SPI      SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件. SPI的作用就是为这些被扩展的API寻找服务实现. 2.SPI和API的使用场景     API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现. 从使用人员上来说,API 直接被应用开发人员使用.

随机推荐