在 JAVA 应用中,常用的 Web 服务器一般由 Tomcat、weblogic、jetty、undertwo等。但从 Java 2019和2020 生态使用报告可以看到,tomcat的用户量对比明显较大,当然这也基于它开源和免费的特点。
Java 2019 年生态圈使用报告
2020 Java 生态系统报告
从软件架构的发展角度来看,软件架构大致经历了如下几个阶段:
从 Java Web 角度来说,架构大致经历了:
从当前企业使用的架构角度来说,使用SSM架构项目比较多,SSH基本被淘汰(大部分是老项目维护),很大一部分企业转向微服务架构了。
基于Spring 生态来说,大部分中小型企业都基本使用SpringBoot,SpringBoot本身集成了 tomcat、jetty和undertwo 容器,那么我们为什么需要花时间来研究tomcat呢?
知识点:
Tomcat 是一个基于JAVA的WEB容器,其实现了JAVA EE中的 Servlet 与 jsp 规范,与Nginx Apache 服务器不同在于一般用于动态请求处理。在架构设计上采用面向组件的方式设计。即整体功能是通过组件的方式拼装完成。另外每个组件都可以被替换以保证灵活性。
通过Tomcat官方可以看到,目前已经更新到Tomcat 10了,但当前大部分企业使用的Tomcat 为8或者9版本。
bin目录存放可执行文件,简要结束常用命令
这里主要解释如下通用的命令,其他命令就不一一介绍
conf文件夹用来存放tomcat相关配置文件
1.catalina.policy
项目安全文件,用来防止欺骗代码或JSP执行带有像System.exit(0)这样的命令的可能影响容器的破坏性代码. 只有当Tomcat用-security命令行参数启动时这个文件才会被使用,即启动tomcat时, startup.sh -security 。
上图中,tomcat容器下部署两个项目,项目1和项目2。由于项目1中有代码System.exit(0),当访问该代码时,该代码会导致整个tomcat停止,从而也导致项目2停止。
为了解决因项目1存在欺骗代码或不安全代码导致损害Tomcat容器,从而影响其他项目正常运行的问题,启动tomcat容器时,加上-security参数就,即startup.sh -security,如此即使项目1中有代码System.exit(0),也只会仅仅停止项目1,而不会影响Tomcat容器,然而起作用的配置文件就是catalina.policy文件。
2.catalina.properties
配置tomcat启动相关信息文件
3.context.xml
监视并加载资源文件,当监视的文件发生发生变化时,自动加载
4.jaspic-providers.xml 和 jaspic-providers.xsd
这两个文件不常用
5.logging.properties
该文件为tomcat日志文件,包括配置tomcat输出格式,日志级别等
6.server.xml
tomcat核心架构主件文件,下面会详细解析。
7.tomcat-users.xml和tomcat-users.xsd
tomcat用户文件,如配置远程登陆账号
tomcat-users.xsd 为tomcat-users.xml描述和约束文件
8.web.xml
tomcat全局配置文件。
lib文件夹主要用来存放tomcat依赖jar包,如下为 tomcat 的lib文件夹下的相关jar包。
每个jar包功能,这里就不讲解了,这里主要分析ecj-4.13.jar,这个jar包起到将.java编译成.class字节码作用。
假设要编译MyTest.java,那么jdk会执行两步:
javac MyTest.java
java MyTest.class
java -jar ecj-4.13.jar MyTest.java
该文件夹表示tomcat日志文件,大致包括如下六类文件:
temp目录用户存放tomcat在运行过程中产生的临时文件。(清空不会对tomcat运行带来影响)。
webapps目录用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用。
当然,你也可以把应用程序放置在磁盘的任意位置,在配置文件中映射好就行。
work目录用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。
清空work目录,然后重启tomcat,可以达到清除缓存的作用。
Server 的基本基本配置:
<Server>
<Listener /><!-- 监听器 -->
<GlobaNamingResources> <!-- 全局资源 -->
</GlobaNamingResources
<Service> <!-- 服务 用于 绑定 连接器与 Engine -->
<Connector 8080/> <!-- 连接器-->
<Connector 8010 /> <!-- 连接器-->
<Connector 8030/> <!-- 连接器-->
<Engine> <!-- 执行引擎-->
<Logger />
<Realm />
<host "www.test.com" appBase=""> <!-- 虚拟主机-->
<Logger /> <!-- 日志配置-->
<Context "/applction" path=""/> <!-- 上下文配置-->
</host>
</Engine>
</Service>
</Server>
root元素:server 的顶级配置
主要属性:
port:执行关闭命令的端口号
shutdown:关闭命令
#基于telent 执行SHUTDOWN 命令即可关闭
telent 127.0.0.1 8005
SHUTDOWN
服务:将多个connector 与一个Engine组合成一个服务,可以配置多个服务。
连接器:用于接收 指定协议下的连接 并指定给唯一的Engine 进行处理。
主要属性:
<Connector port="8860" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8862"
URIEncoding="UTF-8"
useBodyEncodingForURI="true"
compression="on" compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/plain,text/JavaScript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="1024" minSpareThreads="200"
acceptCount="800"
enableLookups="false"
/>
引擎:用于处理连接的执行器,默认的引擎是catalina。一个service 中只能配置一个Engine。
主要属性:name 引擎名称 defaultHost 默认host
虚拟机:基于域名匹配至指定虚拟机。类似于nginx 当中的server,默认的虚拟机是localhost.
<Host name="www.test.com" appBase="/usr/www/test"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="www.luban.com.access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
应用上下文:一个host 下可以配置多个Context ,每个Context 都有其独立的classPath。相互隔离,以免造成ClassPath 冲突。
<Context docBase="hello" path="/h" reloadable="true"/>
阀门:可以理解成request 的过滤器,具体配置要基于具体的Valve 接口的子类。以下即为一个访问日志的Valve.
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="www.luban.com.access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
我们平时启动Tomcat过程是怎么样的?
但是我们在Eclipse 或idea 中启动WEB项目的时候 也是把War包复杂至webapps 目录解压吗?显然不是,其真正做法是在Tomcat程序文件之外创建了一个部署目录,在一般生产环境中也是这么做的 即:Tomcat 程序目录和部署目录分开 。
我们只需要在启动时指定CATALINA_HOME 与 CATALINA_BASE 参数即可实现。
可以编写一个脚本 来实现自定义配置:
更新 启动 脚本:
#!/bin/bash -e
export now_time=$(date +%Y-%m-%d_%H-%M-%S)
echo "deploy time:$now_time"
app=$1
version=$2
mkdir -p war/
#从svn下载程序至 war目录
war=war/${app}_${version}.war
echo "$war"
svn export svn://192.168.0.253/release/${app}_${version}.war $war
deploy_war() {
#解压版本至当前目录
target_dir=war/${app}_${version}_${now_time}
unzip -q $war -d $target_dir
rm -f appwar
ln -sf $target_dir appwar
target_ln=`pwd`/appwar
echo '<?xml version="1.0" encoding="UTF-8" ?>
<Context docBase="'$target_ln'" allowLinking="false">
</Context>' > conf/Catalina/localhost/ROOT.xml
#重启Tomcat服务
./tomcat.sh restart
}
deploy_war
```
自动部署脚本:
#!/bin/bash -e
export now_time=$(date +%Y-%m-%d_%H-%M-%S)
echo "deploy time:$now_time"
app=$1
version=$2
mkdir -p war/
#从svn下载程序至 war目录
war=war/${app}_${version}.war
echo "$war"
svn export svn://192.168.0.253/release/${app}_${version}.war $war
deploy_war() {
#解压版本至当前目录
target_dir=war/${app}_${version}_${now_time}
unzip -q $war -d $target_dir
rm -f appwar
ln -sf $target_dir appwar
target_ln=`pwd`/appwar
echo '<?xml version="1.0" encoding="UTF-8" ?>
<Context docBase="'$target_ln'" allowLinking="false">
</Context>' > conf/Catalina/localhost/ROOT.xml
#重启Tomcat服务
./tomcat.sh restart
}
deploy_war
```
IO是指为数据传输所提供的输入输出流,其输入输出对象可以是:文件、网络服务、内存等。
提问:
假设应用在从硬盘中读取一个大文件过程中,此时CPU会与硬盘一样处于高负荷状态么?
演示:
演示结果:CPU 没有太高的增长
通常情况下IO操作是比较耗时的,所以为了高效的使用硬件,应用程序可以用一个专门线程进行IO操作,而另外一个线程则利用CPU的空闲去做其它计算。这种为提高应用执行效率而采用的IO操作方法即为IO模型。
BIO
阻塞式IO,即Tomcat使用传统的java.io进行操作。该模式下每个请求都会创建一个线程,对性能开销大,不适合高并发场景。优点是稳定,适合连接数目小且固定架构。
NIO
非阻塞式IO,jdk1.4 之后实现的新IO。该模式基于多路复用选择器监测连接状态在通知线程处理,从而达到非阻塞的目的。比传统BIO能更好的支持并发性能。Tomcat 8.0之后默认采用该模式
APR
全称是 Apache Portable Runtime/Apache可移植运行库),是Apache HTTP服务器的支持库。可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作。使用需要编译安装APR 库
AIO
异步非阻塞式IO,jdk1.7后之支持 。与nio不同在于不需要多路复用选择器,而是请求处理线程执行完成进行回调调制,已继续执行后续操作。Tomcat 8之后支持。
配置 server.xml 文件当中的 <Connector protocol="HTTP/1.1"> 修改即可。
默认配置 8.0 protocol=“HTTP/1.1” 8.0 之前是 BIO, 8.0 之后是 NIO
protocol=“org.apache.coyote.http11.Http11Protocol”
protocol=“org.apache.coyote.http11.Http11NioProtocol”
protocol=“org.apache.coyote.http11.Http11Nio2Protocol”
protocol=“org.apache.coyote.http11.Http11AprProtocol”
分别演示在高并发场景下BIO与NIO的线程数的变化?
BIO 配置
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Protocol"
connectionTimeout="20000"
redirectPort="8443"
compression="on" compressionMinSize="1024"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="500" minSpareThreads="1"/>
NIO配置
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
compression="on" compressionMinSize="1024"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="500" minSpareThreads="1"/>
演示数据:
生成环境重要因素:
源代码地址:https://github.com/org-hejianhui/bit-bigdata-transmission
BIO 线程模型
BIO 源码
线程组:
Accept 线程组 acceptorThreadCount 默认1个
exec 线程组 maxThread
JIoEndpoint
Acceptor extends Runnable
SocketProcessor extends Runnable
NIO 线程模型
NIO 线程模型
Accept 线程组 默认两个轮询器
Poller Selector PollerEvent轮询线程状态
SocketProcessor
BIO
线程数量 会受到 客户端阻塞、网络延迟、业务处理慢===>线程数会更多。
NIO
线程数量 会受到业务处理慢===>线程数会更多。
是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 .class,也可以是 jar 包里的 .class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。
jvm里ClassLoader的层次结构
类加载器层次结构
BootstrapClassLoader(启动类加载器)
称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url.toExternalForm());
}
程序执行结果如下:
file:/Library/Java/JavaVirtualmachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes
从rt.jar中选择String类,看一下String类的类加载器是什么
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
执行结果如下:
null
可知由于BootstrapClassLoader对Java不可见,所以返回了null,我们也可以通过某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassLoader进行加载的依据。
ExtensionClassLoader
ExtClassLoader称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)。
ExtClassLoader的类扫描路径通过执行下面代码来看一下:
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
执行结果如下(Mac系统):
/Users/hjh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
jre/lib/ext路径下内容为:
从上面的路径中随意选择一个类,来看看它的类加载器是什么:
sun.misc.Launcher$ExtClassLoader@4439f31e
null
从上面的程序运行结果可知ExtClassLoader的父加载器为null,之前说过BootstrapClassLoader对Java不可见,所以返回了null。ExtClassLoader的父加载器返回的是null,那是否说明ExtClassLoader的父加载器是BootstrapClassLoader?
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的现象
AppClassLoader
才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
public class AppClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
}
}
输出结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
以上结论说明调用ClassLoader.getSystemClassLoader()可以获得AppClassLoader类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
通过查看ClassLoader的源码发现并且在没有特定说明的情况下,用户自定义的任何类加载器都将该类加载器作为自定义类加载器的父加载器。
通过执行上面的代码即可获得classpath的加载路径。
在上面的main函数的类的加载就是使用AppClassLoader加载器进行加载的,可以通过执行下面的代码得出这个结论
public class AppClassLoaderTest {
public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
}
private static class Test {
}
}
执行结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2d209079
从上面的运行结果可以得知AppClassLoader的父加载器是ExtClassLoader
在Tomcat中,默认的行为是先尝试在Bootstrap和Extension中进行类型加载,如果加载不到则在Webapp ClassLoader中进行加载,如果还是找不到则在Common中进行查找。
NoClassDefFoundError是在开发JavaEE程序中常见的一种问题。该问题会随着你所使用的JavaEE中间件环境的复杂度以及应用本身的体量变得更加复杂,尤其是现在的JavaEE服务器具有大量的类加载器。
在JavaDoc中对NoClassDefFoundError的产生是由于JVM或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。
这一刻IDE是没有出错提醒的,但是在运行时却出现了错误。
在另一个场景中,我们可能遇到了另一个错误,也就是NoSuchMethodError。
NoSuchMethodError代表这个类型确实存在,但是一个不正确的版本被加载了。
ClassCastException,在一个类加载器的情况下,一般出现这种错误都会是在转型操作时,比如:A a = (A) method();,很容易判断出来method()方法返回的类型不是类型A,但是在 JavaEE 多个类加载器的环境下就会出现一些难以定位的情况。
部分图片来源于网络,版权归原作者,侵删。