Java web开发技术总结

Java Web开发

  • 一、web请求的过程
    • 1、传统C/S网络架构
    • 2、B/S网络架构
    • 3、HTTP解析
      • 3.1常见的HTTP请求头
      • 3.2常见的HTTP响应头
      • 3.3常见的HTTP状态码
        • 400 Bad Request
        • 403 Forbidden
        • 404 Not Found
        • 500 Internal Server Error
    • 4、CDN工作机制
    • 5、负载均衡
    • 6、CDN动态加速
  • 二、Java I/O的工作机制
    • 1、关于乱码的问题,为啥会出现乱码?
      • 1.1为什么会有操作字符的接口?
      • 1.2字节与字符的转换接口
    • 2、磁盘I/O的工作机制
      • 2.1标准访问文件的方式
      • 2.2直接 I/O 的方式(适用于数据库场景)
      • 2.3同步访问文件的方式(web中几乎没有应用场景)
      • 2.4异步访问文件的方式
      • 2.5 内存映射的方式(kafka设计思想的零拷贝)
    • 3、Java访问磁盘文件(File类并不是真正的文件对象,而是IO对象)
      • 3.1 FileDesciptor类(这才是真正的文件对象)
    • 4、Java的序列化技术
      • 4.1 static,transient的变量不能被序列化
    • 5、网络I/O工作机制
      • 5.1Java Socket工作机制
      • 5.2建立通信链路
      • 5.3 数据传输
    • 6、Java的NIO
    • 6.1 NIO的工作机制
    • 7、I/O调优
      • 7.1 磁盘I/O优化
      • 7.2 TCP网络参数调优
      • 7.3网络I/O优化
  • 三、Java web 中的中文编码问题
  • 四、Javac编译原理
  • 五、class文件结构
  • 六、classloader类加载器工作机制
    • 0、啥是类加载器
    • 1、classloader有3种功能:
    • 2、ClassLoader类结构分析
    • 3、Java语言系统自带有三个类加载器:
      • Bootstrap ClassLoader最顶层的加载类(JVM自带)
      • ExtClassLoader 扩展的类加载器
      • AppClassLoader 应用类加载器
      • 为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢
    • 4、双亲委托
      • Bootstrap ClassLoader是由C++编写的。
      • 双亲委托
    • 5、自定义classloader
      • 自定义步骤
      • defineClass()
      • 注意点:
      • 自定义ClassLoader示例之DiskClassLoader
      • Test.java
      • DiskClassLoader
      • 测试
  • 七、JVM体系结构和工作方式
  • 八、JVM内存管理
  • 九、Servlet工作原理
  • 十、session和cookie
  • 十一、Tomcat系统架构
  • 十二、Spring设计理念和SpringMVC工作机制
  • 十三、Mybatis系统架构和设计原理

一、web请求的过程

1、传统C/S网络架构

传统C/S互联网应用程序采用长连接的交互模式。

2、B/S网络架构

客户端使用统一的浏览器(Browser),
服务端使用的是统一的HTTP,无状态、短链接的通信方式。一次请求,即完成了一次交互。
当前互联网每天都会处理上亿的请求,不可能访问一次后就一直保持这个链接。

3、HTTP解析

理解HTTP,最重要的是要熟悉HTTP中HTTP Header,HTTP Header控制着互联网成千上万的用户的数据传输。最关键的是它控制着用户浏览器的渲染行为和服务器的执行逻辑

3.1常见的HTTP请求头

Accept-Charset:告诉服务器,客户端采用的编码。
 
Accept-Encoding:告诉服务器,客户机支持的数据压缩格式。
 
Accept-Language:告诉服务器,客户机的语言环境。
 
Host:客户机通过这个头告诉服务器,想访问的主机名。

User-Agent:客户机通过这个头告诉服务器,客户机的软件环境。

Connection:客户机通过这个头告诉服务器,请求完后是关闭还是保持链接。

3.2常见的HTTP响应头

Server:服务器通过这个头,告诉浏览器服务器的类型

Content-Encoding:服务器通过这个头,告诉浏览器数据采用的压缩格式。

Content-Length:服务器通过这个头,告诉浏览器回送数据的长度。

Content-Language:服务器通过这个头,告诉服务器的语言环境。

Content-Type:服务器通过这个头,回送数据的类型

3.3常见的HTTP状态码

Java web开发技术总结_第1张图片

400 Bad Request

该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求 的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态 码。

403 Forbidden

该状态码表明对请求资源的访问被服务器拒绝了。

服务器端没有必要 给出拒绝的详细理由,但如果想作说明的话,可以在实体的主体部分 66 对原因进行描述,这样就能让用户看到了。

未获得文件系统的访问授权,访问权限出现某些问题(从未授权的发 送源 IP 地址试图访问)等列举的情况都可能是发生 403 的原因。

404 Not Found

该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服 务器端拒绝请求且不想说明理由时使用。

500 Internal Server Error

重要程度:高。
这是一个通用的服务器错误响应。对于大多数web框架,如果在执行请求处理代码时遇到了异常,它们就发送此响应代码。

4、CDN工作机制

CDN,即内容分布网络,它是构筑在现有Internet上的一种先进的流量分配网络。
其目的是通过在现有的Internet中增加一层新的网络架构,将网络的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
可以做这样一个比喻:CDN = 镜像(Mirror)+ 缓存(Cache)+ 整体负载均衡(GSLB)。因此,CDN可以明显提高Internet中信息流动的效率。

目前CDN以缓存网站中的静态数据为主,如CSS、JS、图片和静态页面等数据。当用户请求动态内容时,先从CDN上下载静态数据,从而加速网页数据内容的下载速度。
如天猫和京东有90%以上数据是由CDN来提供的。

CDN的目标:

可扩展:性能可扩展性:应对新增的大量数据、用户和事务的扩展能力。成本可扩展性:用低廉的运营成本提供动态的服务能力和高质量的内容分发。

安全性:强调提供物理设备、网络、软件、数据和服务过程的安全性,减少因为DDoS攻击或者其他恶意行为造成商业网站的业务中断。

可靠性、响应和执行。服务可用性指能够处理可能的故障和用户体验下降的问题,通过负载均衡及时提供网络的容错机制。

CDN架构
Java web开发技术总结_第2张图片
拿到DNS解析的结果,用户就直接去这个CDN节点访问这个静态文件了。

5、负载均衡

通常有3种,分别是链路负载均衡、集群负载均衡、操作系统负载均衡。

链路负载均衡:
		就是前面提到的通过DNS解析成不同的IP,然后根据这个ip来访问不同的目标服务器。负载均衡是由DNS的解析来完成的,用户最终访问那个web server是由DNS server来控制的。
		
集群负载均衡:
		一般分为硬件负载均衡和软件负载均衡。
		
		硬件负载均衡,这台设备非常昂贵,如F5。
		
		软件负载均衡,一般用Nginx。
		
操作系统负载均衡:
		利用操作系统级别的软中断或者硬件中断来达到负载均衡,如设置多队列网卡等实现。

6、CDN动态加速

CDN动态加速技术是当前比较流行的一种技术,它的技术原理,就是在CDN的DNS解析中通过动态的链路探测来寻找回源最好的一条路径,然后通过DNS的调度将所有的请求调度到选定的这条路上回源。
由于CDN节点是遍布全国的,所以用户接入一个CDN节点后,可以选择一条从离用户最近的CDN节点到源站链路最好的路径让用户走。一个原则,看哪个链路的总耗时最短。

二、Java I/O的工作机制

IO很容易成为系统的瓶颈,Java的io包下有80多个类。

基于字节操作的IO接口:InputStream 和 OutputStream
基于字符操作的IO接口:Reader 和 Writer
基于磁盘操作的IO接口: File
基于网络操作的IO接口:Socket

InputStream 和 OutputStream、Reader 和 Writer主要是传输数据的格式,如:传输是字节还是字符?
File和Socket主要是传输数据的方式,如:是磁盘IO还是网络IO。

虽然Socket类不在java.io包下,但我们把它们划分到一起。
笔者认为I/O的核心问题:要么是数据格式影响I/O操作,要么是传输方式影响I/O操作。I/O只是人与机器或者机器与机器交互的手段。

1、关于乱码的问题,为啥会出现乱码?

不管磁盘还是网络传输,最小的存储单元都是字节,而不是字符。所以I/O的操作都是字节而不是字符。

1.1为什么会有操作字符的接口?

因为我们在程序中常操作的都是字符的形式,为了操作方便当然要提供一个直接写字符的I/O接口。

1.2字节与字符的转换接口

我们知道从字符到字节必须经过编码转换,而这个编码又非常耗时,而且会经常出现乱码。所以I/O的编码问题经常是让人头疼的问题。
另外,数据持久化和网络传输都是以字节进行的,所以必须要有字符到字节和从字节到字符的转化。

InputStreamReader类是从字节到字符的转化桥梁,从InputStream到Reader的过程要指定编码字符集,否则Linux将采用操作系统默认的字符集ISO-8859-1,很可能会出现乱码问题。
StreamDecoder正是完成字节到字符的解码实现类。

OutputStreamWriter类完成了从字符到字节的编码过程,由StreamEncoder完成编码过程。

2、磁盘I/O的工作机制

读取和写入文件I/O操作都要调用操作操作系统提供的接口,因为磁盘是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作。读和写分别对应read()和write()函数。

然而只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题。这是操作系统为了保护系统本身的运行安全,而将内核程序运行使用的内存空间和用户程序运行的内存空间进行隔离造成的。这样虽然保证了内核程序运行的安全性,但是必然存在数据可能需要从内核空间向用户空间复制的问题。

2.1标准访问文件的方式

标准文件访问方式是当应用程序调用read()时Linux操作系统检查在内核中的高速缓存中有没有需要的数据,如果已经缓存了那么直接从缓存中返回,如果没有则从硬盘中读取,然后缓存在操作系统的缓存中。

写入的方式是,用户的应用程序调用 write() 接口将数据从用户地址空间复制到内核地址空间的缓存中。这时对用户程序来说写操作就已经完成了,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用 sync 同步命令。

标准访问文件的方式如下图所示:
Java web开发技术总结_第3张图片

2.2直接 I/O 的方式(适用于数据库场景)

直接 I/O 方式就是应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区,这样做的目的就是减少一次从内核缓冲区到用户程序缓存的数据复制

此种方式通常是在对数据的缓存管理由应用程序实现的数据库管理系统中。如在数据库管理系统中,系统明确的知道应该缓存哪些数据,应该失效哪些数据,还可以对一些热点的数据进行预加载,提前将热点数据加载到内存,可以加速数据的访问效率。在这些情况下,如果是由操作系统进行缓存,则很难做到,因为操作系统并不知道哪些是热点数据,哪些数据是访问一次后再也不会访问了,操作系统就是简单的缓存最近一次从磁盘读取的数据。

但是直接 I/O 也有负面的影响,如果访问的数据不再应用程序缓存中,则每次数据的加载都需要从磁盘读取,样加载的话速度非常的慢,通常是直接 I/O 与 异步 I/O 结合使用,会得到较好的性能。

直接 I/O 的方式如下图所示:
Java web开发技术总结_第4张图片

2.3同步访问文件的方式(web中几乎没有应用场景)

同步访问文件的方式就是数据的读取和写入都是同步操作的,它与标准访问文件的方式不同的是,只有当数据被成功写到磁盘时才返回给应用程序成功的标志。

这种访问文件的方式性能比较差,只有在一些数据安全性要求比较高的场景中才会使用,而且通常这种方式的硬件都是定制的。

同步访问文件的方式如下图所示:
Java web开发技术总结_第5张图片

2.4异步访问文件的方式

异步访问文件的方式就是当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。这种方式可以明显的提高应用程序的效率,但是不会改变访问文件的效率。

异步访问文件的方式如下图所示:
Java web开发技术总结_第6张图片

2.5 内存映射的方式(kafka设计思想的零拷贝)

内存映射的方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的某一段数据。

这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。

内存映射的方式如下图所示:

Java web开发技术总结_第7张图片

3、Java访问磁盘文件(File类并不是真正的文件对象,而是IO对象)

数据在磁盘中的唯一最小的描述就是文件,正如Unix网络编程所说的那样,一切皆为文件,正如Java中的一切皆为对象。上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单位。

注意:
在Java中,通常File并不代表一个真实存在的文件对象,当你指定一个路径描述符时,它会返回一个代表这个路径的一个虚拟对象,可能是一个真实存在的文件或者只是一个包含多个文件的目录.这样设计的原因是我们并不关心这个文件是否真的存在,只是关心它如何操作.

3.1 FileDesciptor类(这才是真正的文件对象)

何时会真正检查一个文件存不存在?
就是当我们真正读取这个文件时,才需要检查这个文件是不是真的存在.

下面就举一个例子说明:


FileInputStream类是操作一个文件的接口,在创建一个FileInputStream对象时会创建一个FileDesciptor对象,这个对象就是真正代表一个存在的文件对象的描述.

当在操作一个文件对象时可以通过getFD()方法获取真正操作的与底层操作系统关联的文件描述.例如,可以调用FileDesciptor.sync()方法将操作系统缓存中的数据强制刷新到物理磁盘上.

如果我们需要从磁盘中读取一段文本字符那么就会有以下过程:


1、当传入一个文件路径时将会根据这个路径创建一个File对象来标识这个文件,然后根据这个File对象创建真正读取文件的操作对象.
2、这时将会真正创建一个关联真实存在的磁盘文件的文件描述符FileDescriptor,通过这个对象可以直接控制这个磁盘文件.
3、由于我们需要读取的是字符格式,所以需要StreamDecoder类将byte解码为char格式.
至于如何从磁盘驱动器上读取一段数据,操作系统会帮我们完成.

Java web开发技术总结_第8张图片

4、Java的序列化技术

Java序列化就是讲一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。
需要持久化,对象必须继承Java.io.Serializable接口。反序列化则是相反的过程,将这个字节数组再重新构造成对象。

Serializable接口没有任何东西,
无需重写任何方法。它只是一个标识。

我们知道反序列化时,必须有原始类作为模板,才可以把这个对象还原。
这个过程我们可以知道,序列化的数据并不像class字节码文件那样保存类的完整的结构信息。

4.1 static,transient的变量不能被序列化

一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
transient关键字只能修饰变量,而不能修饰方法和类。

注意:
本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化

5、网络I/O工作机制

当我们压测一个web应用程序时可能遇到CPU、网卡、带宽都不是瓶颈,但是性能就是压不上去,这时候去观察一下网络连接情况。可能发现由于网络连接的并发数不够导致连接都处于TIME_WAI状态,这时候就要做tcp网络参数调优了。

5.1Java Socket工作机制

Socket这个概念没有对应到一个具体的实体,他描述计算机之间完成相互通信的一种抽象功能。打个比方,可以把Socket比作是两个城市之间的交通工具,交通工具有多种,每种交通工具都有相应的交通规则。Socket的交通规则都是基于TCP/IP的流套接字。
在一台计算机上可能运行多个web应用程序,如何才能制定和应用程序通信就是通过端口号来指定。这样就可以通过一个Socket实例对象来唯一代表一个主机上的应用程序的通信链路了。

5.2建立通信链路

我们在 new 一个Socket的实例对象的构造函数正确返回对象之前,就要进行TCP的3次握手协议,TCP握手协议完成后,Socket实例对象将创建完成,否则将抛出IOException错误。

与之对应的服务器端将创建一个ServerSocket实例,创建ServerSocket比较简单,只要指定的端口号没有占用,一般实例创建都会成功。之后将调用accept()方法,将进入阻塞状态,等待客户端请求。
当一个请求到来时,将为这个连接创建一个新的套接字Socket数据结构,该套接字数据的信息包含的地址和端口,正是请求源地址和端口。这个新创建的数据结构将会关联到ServerSocket实例的一个未完成的连接数据结构列表中。
注意:这一刻服务端与之对应的Socket实例并没有创建完成,而要等到与客户端的3次握手完成后,这个服务端的Socket实例才会返回。并把这个Socket实例对应的数据结构从未完成列表移到已完成列表。
所以,与ServerSocket所关联的列表中每个数据结构都代表一个客户端建立的TCP连接

5.3 数据传输

6、Java的NIO

BIO即阻塞IO,不管是磁盘IO和网络IO,数据在写入OutputStream和从InputStream读取时都有可能会阻塞,一旦阻塞,线程将失去CPU的使用权。

6.1 NIO的工作机制

7、I/O调优

7.1 磁盘I/O优化

7.2 TCP网络参数调优

7.3网络I/O优化

三、Java web 中的中文编码问题

四、Javac编译原理

五、class文件结构

六、classloader类加载器工作机制

ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。比如你公司要研发一个rpc框架,你必须要用到classloader。

理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。

但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。

0、啥是类加载器

sun公司的虚拟机规范设计团队给出:“通过一个类的全限定名来获取描述这个类的二进制字节流”,这个动作放到Java虚拟机的外部去实现,以便于让应用程序自己决定如何去获取所需要的类。实现这个动作功能的代码成为“类加载器”

如果你不明白这句话,我给你解释一下。
我问你:“Java如何创建1个对象?”
你说:“通过new的方式或者反射的方式,如Class.forName("com.alibaba.pojo.Order")”
我又问:“还有其它方式吗”
。。。。。。这个问题,很多人都会摇头。

今天,我给出一个全新的答案:“通过二进制字节流创建对象。”
new和反射只不过是创建对象的手段,比如你从网上接到一个对象的二进制字节流,然后通过类名反序列化,不也可以对象。

类加载器就是获取这个类的class字节码文件的二进制字节流。

从Java虚拟机的角度讲,只存在2种类加载器:
一种是启动类加载器(Boostrap ClassLoader)

Boostrap这个类加载器是由C++实现的,是虚拟机JVM的一部分。所以你在jdk的源代码库中找不到,当年自己也傻乎乎找了半天。

另一种就是sun虚拟机规范中提到的其它加载器。这类加载器都是用Java语言实现的。底下我们讨论都是Java实现的类加载器。

1、classloader有3种功能:

1、将Class文件信息加载到JVM中。
2、还有一个重要的作用就是审查每个类该有谁加载,它是一种父级优先的机制。
3、将Class字节码重新解析成JVM统一要求的对象格式。

类的加载方式,有两种:

1、隐式装载-->new的方式, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。

2、显式装载-->反射方式, 通过class.forName()等方法,显式加载需要的类

当然了,调用一个类的静态方法,又或者在new一个子类的时候,如果发现父类class还没有加载则先触发父类的加载。

2、ClassLoader类结构分析

ClassLoader是一个抽象的Java类。如果去研发框架,而不是去写业务代码,我们经常会用到或扩展ClassLoader。

如果我们要实现自己自定义的ClassLoader,就是去继承这个ClassLoader抽象类。

主要用到4个方法:

findClass(String) : 根据传参路径,找到这个class文件。

loadClass(String):把这个类class字节码打进JVM虚拟机,这是一个IO操作。

defineClass(byte[],int,int):用来将byte字节流解析成JVM能够识别的class对象。defineClass方法通常是和findClass方法一起使用。

resloveClass(Class):在对象真正实例化时才进行
注意:如果直接调用defineClass方法生成类的Class对象,这个类的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行。
				

3、Java语言系统自带有三个类加载器:

Bootstrap ClassLoader最顶层的加载类(JVM自带)

主要加载jdk核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

ExtClassLoader 扩展的类加载器

加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。如:maven中pom文件中的jar包

AppClassLoader 应用类加载器

加载当前应用的classpath的所有类。比如我们写的web应用的业务代码controller、service等代码

说一下ExtClassLoader和AppClassLoader,找了半天jdk代码没有找到的问题。
这两个类在Launcher类中,是个静态内部类。

如图Launcher类:
Java web开发技术总结_第9张图片
如图ExtClassLoader类:
Java web开发技术总结_第10张图片
上面的代码很精简,我们可以看到:
1、Launcher初始化了ExtClassLoader和AppClassLoader。
2、ExtClassLoader和AppClassLoader都继承了URLClassLoader,而URLClassLoader又实现了抽象类ClassLoader。
继承关系如图:
Java web开发技术总结_第11张图片

为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢

先从URLClassLoader说起,URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。
Java web开发技术总结_第12张图片
我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

1、由外部类创建ClassLoader时直接指定一个ClassLoader为parent。

2、由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,我们上面已经粘贴过Launcher的部分代码。

我们需要注意的是:

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。

ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。

public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);   
}

对应的代码

public  URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
     super(parent);
}

答案已经很明了了,ExtClassLoader的parent为null。

上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合我们之前编写的测试代码。

不过,细心的同学发现,还是有疑问的我们只看到ExtClassLoader和AppClassLoader的创建,那么BootstrapClassLoader呢?

还有,ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?

我们继续往下进行。

4、双亲委托

Bootstrap ClassLoader是由C++编写的。

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。
这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。具体是什么原因,很快就知道答案了。

双亲委托

双亲委托。
我们终于来到了这一步了。

一个类加载器查找class和resource时,是通过“委托模式”进行的,
它首先判断这个class是不是已经加载成功,
如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,
如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:
Java web开发技术总结_第13张图片
大家可以看到2根箭头,蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。
用序列描述一下:

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
路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。

Java web开发技术总结_第14张图片
上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

5、自定义classloader

注意:
如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。除非你水平逆天了,写的比jdk还要好。

不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?

如果要这样做的话,需要我们自定义一个classloader。

自定义步骤

1、编写一个类继承自ClassLoader抽象类。

2、复写它的findClass()方法。

3、在findClass()方法中调用defineClass()。

defineClass()

这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。

注意点:

一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader

上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

自定义ClassLoader示例之DiskClassLoader

假设我们需要一个自定义的classloader,默认加载路径为D:\lib
下的jar包和资源。

我们写编写一个测试用的类文件,Test.java

Test.java

package com.frank.test;

public class Test {
	
	public void say(){
		System.out.println("Say Hello");
	}

}

然后将它编译过年class文件Test.class放到D:\lib
这个路径下。

DiskClassLoader

我们编写DiskClassLoader的代码。

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


public class DiskClassLoader extends ClassLoader {
	
	private String mLibPath;
	
	public DiskClassLoader(String path) {
		// TODO Auto-generated constructor stub
		mLibPath = path;
	}

	@Override
	protected Class findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		
		String fileName = getFileName(name);
		
		File file = new File(mLibPath,fileName);
		
		try {
			FileInputStream is = new FileInputStream(file);
			
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			int len = 0;
	        try {
	            while ((len = is.read()) != -1) {
	            	bos.write(len);
	            }
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	        
	        byte[] data = bos.toByteArray();
	        is.close();
	        bos.close();
	        
	        return defineClass(name,data,0,data.length);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return super.findClass(name);
	}

	//获取要加载 的class文件名
	private String getFileName(String name) {
		// TODO Auto-generated method stub
		int index = name.lastIndexOf('.');
		if(index == -1){ 
			return name+".class";
		}else{
			return name.substring(index+1)+".class";
		}
	}
	
}


我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。

测试

现在我们要编写测试代码。我们知道如果调用一个Test对象的say方法,它会输出"Say Hello"这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们编写的DiskClassLoader能不能顺利完成任务呢?我们拭目以待。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
	
		//创建自定义classloader对象。
		DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
		try {
			//加载class文件
			Class c = diskLoader.loadClass("com.frank.test.Test");
			
			if(c != null){
				try {
					Object obj = c.newInstance();
					Method method = c.getDeclaredMethod("say",null);
					//通过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

}


七、JVM体系结构和工作方式

八、JVM内存管理

九、Servlet工作原理

十、session和cookie

十一、Tomcat系统架构

十二、Spring设计理念和SpringMVC工作机制

去面试人常问“Spring BeanFactory与FactoryBean的区别?”

BeanFactory就是个工厂类,又叫bean工厂,专门生产(创建)bean。

BeanFactory定义了 IOC 容器的最基本形式,并提供了 IOC 容器应遵守的的最基本的接口,也就是 Spring IOC 所遵守的最底层和最基本的编程规范。
在 Spring 代码中, BeanFactory 只是个接口,并不是 IOC 容器的具体实现,但是 Spring 容器给出了很多种实现,如 DefaultListableBeanFactory 、 XmlBeanFactory 、 ApplicationContext 等,都是附加了某种功能的实现。

Spring的BeanFacotry是一个类工厂,使用它来创建各种类型的Bean,最主要的方法就是getBean(String beanName),该方法从容器中返回特定名称的Bean,只不过其中有一种Bean是FacotryBean.

一个Bean 要想成为FacotryBean,必须实现FactoryBean 这个接口。

FactoryBean又是啥?

一般情况下,Spring 通过反射机制利用 的 class 属性指定实现类实例化 Bean ,在某些情况下,实例化 Bean 过程比较复杂,如果按照传统的方式,则需要在 中提供大量的配置信息。

配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。 Spring 为此提供了一个 org.springframework.bean.factory.FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑。

FactoryBean接口对于 Spring 框架来说占用重要的地位, Spring 自身就提供了 70 多个 FactoryBean 的实现。它们隐藏了实例化一些复杂 Bean 的细节,给上层应用带来了便利。从 Spring 3.0 开始, FactoryBean 开始支持泛型,即接口声明改为 FactoryBean 的形式:

package org.springframework.beans.factory;

public interface FactoryBean {

    T getObject() throws Exception;

    Class getObjectType();

    boolean isSingleton();
}

1)Object getObject():返回由FactoryBean创建的Bean的实例,如果isSingleton()方法返回true,是单例的实例,该实例将放入Spring的缓冲池中;

2)boolean isSingleton*():确定由FactoryBean创建的Bean的作用域是singleton还是prototype;
3) getObjectType():返回FactoryBean创建的Bean的类型。

FactoryBean 是一直特殊的bean,它实际上也是一个工厂,我们在通过FactoryBeanName得到的Bean,是FacotryBean创建的Bean,即它通过getObject()创建的Bean.我们要想得到FactoryBean本身,必须通过&FactoryBeanName得到,即在BeanFactory中通过getBean(&FactoryBeanName)来得到 FactoryBean

注:在spring 中是通过BeanFactoryUtils.isFactoryDereference()来判断一个Bean是否是FactoryBean.

spring 内部实现中应该是在通过BeanFacotry 的getBean(String beanName) 来得到Bean时,如果这个Bean是一个FactoryBean,则要先调用getObject()再把它生成的Bean返回,否者直接返回Bean.

十三、Mybatis系统架构和设计原理





你可能感兴趣的:(JavaWeb开发)