上一篇文章讲述了Tomcat作为一个应用服务器,他是如何将前端发送的请求进行接收并且将请求发送给后端项目的。这一篇呢,我们就来学习一下Tomcat中用到了哪些Java机制,他又对Java中的机制有哪些改造。以及Tomcat的热部署是什么,该如何配置,有什么用处。
误退出JVM
我们在正常开发Servlet的时候,如果在业务逻辑处理方法中添加了如下代码的时候会产生一个问题。
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 退出JVM虚拟机
System.exit(1);
}
如果在代码中有如下方法的时候,JVM会直接退出,Tomcat也会随之关闭,这种情况我们是不想发生的,所以我们要通过权限限制,不给Tomcat执行此方法的权力。
通过这个例子我们顺便了解一下tomcat启动的脚本文件。
我们一般会通过startup.bat(Windows环境)或startup.sh(Linux环境)命令启动Tomcat,那么我们就用bat文件举例吧。
再向下翻我们可以看到安全管理的设置。
在我们启动Tomcat的时候就可以通过startup.bat -security命令来启用我们的安全管理。设置Tomcat可以使用的权限。
而Tomcat的安全管理权限也在catalina.policy文件下保存着。
通过编写此文件,我们就可以自由操作配置Tomcat的使用权限了。
在编写普通Java程序的时候我们可以自由配置使用权限,通过如下命令。
# 开启安全管理
-Djava.security.manager
# 安全管理配置文件路径
-Djava.security.policy=路径信息
在开启安全管理之后,我们一旦执行到如下代码时,就会进行权限检查。
检查失败时会提示不具有对应的权限,此时我们可以通过permission关键字 加上报错信息中access denied 关键字后面的信息,将其加入到我们创建的policy文件中(多个permission时使用;隔开,类名不需要引号,但后面的参数需要添加引号)获取该行为的权限。
如果我们使用的项目没有权限,就算调用有权限的jar包下面的方法也是无法调用的。
此时我们如果确实需要去调用,可以使用如下方法跳过权限检查。
// 通过此方法跳过权限检查,进行调用
// 此方法需要在有权限的类或jar包中使用,相当于向外部提供了一个跳过安全检查的方法入口
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public String run() {
// 执行我们需要调用但没有权限的方法
return null;
}
});
JVM的类加载采用了双亲委派机制,具体内容在我之前的文章中有较详细的说明,不太懂的小伙伴们可以前往查看,这里我们就主要讲一下Tomcat对双亲委派机制有什么样的改造。
工作中需要用到的Java知识(JVM上篇——JVM概念讲解)
简单来说双亲委派机制就是保证Java中的类不会被开发者随意篡改。
如果我们有自己需要加载的类就需要自定义类加载器,下面为样例代码
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* 自定义类加载器,通过继承ClassLoader类
*/
public class MyClassLoader extends ClassLoader{
// 类名
private String name;
// 构造器
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
/**
* 此方法用于类加载器进行类查找,只实现此方法可以进行类查找
* 类加载还是遵循双亲委派机制
* @param name
* The binary name of the class
*
* @return
*/
@Override
protected Class> findClass(String name) {
// 获取加载类的class文件字节数组
byte[] bytes = getBytes("文件路径");
// 将字节数组转换为Class对象并返回
return this.defineClass(name, bytes, 0, bytes.length);
}
public static void main(String[] args) {
// 创建自定义类加载器
// 不同的类加载器对象(只要是new出来的两个或多个类加载器),他们去加载同一个类会生成两个或多个的不同类对象
MyClassLoader myClassLoader = new MyClassLoader(MyClassLoader.class.getClassLoader(), "MyClassLoader");
MyClassLoader myClassLoaderTest = new MyClassLoader(MyClassLoader.class.getClassLoader(), "MyClassLoader");
try{
// 加载指定类
Class> diyClass = myClassLoader.loadClass("className");
Class> diyClassTest = myClassLoaderTest.loadClass("className");
System.out.println("两个对象是否相同" + diyClass.equals(diyClassTest));
// 两者是不同的,他们的内存地址也是不同的
System.out.println(diyClass.toString());
System.out.println(diyClassTest.toString());
// 类初始化
diyClass.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 如果不想遵循双亲委派机制的话就需要重写类加载的方法
* 但是我们不使用双亲委派的机制加载的时候可能会因为我们加载的类调用了根加载器的方法,导致我们自定义的加载器无法启动
* @param name
* The binary name of the class
*
* @param resolve
* If true then resolve the class
*
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
/**
* 将文件转换为字节数组
* @param url 文件路径
* @return 返回字节数组
*/
public byte[] getBytes(String url) {
try(InputStream in = Files.newInputStream(Paths.get(url));
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream()){
int myByte;
while ((myByte = in.read()) != -1) {
byteArrayStream.write(myByte);
}
return byteArrayStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
热部署
热部署表示重新部署应用,执行主体是Host(主机)。
原理是监听webapps下面的应用文件夹的修改时间,如果存在修改则需要重新部署。
Tomcat默认是开启热部署的,我们可以通过如下方式进行关闭。
通过server.xml文件配置,将红框内属性设置为false。
热加载
热加载表示重新加载class文件,执行主体是Context(应用上下文)。
原理是监听webapps下面的应用文件夹下WEB-INF文件夹的修改时间,如果存在修改则需要重新加载。
Tomcat默认是关闭热加载的,我们可以通过如下几种方式进行开启。
1.通过server.xml文件开启
2.通过context.xml文件开启
3.在conf路径下创建对应Context.xml文件开启
热加载入口java.org.apache.catalina.loader.WebappLoader.backgroundProcess方法。
热加载方法主要功能如下:
Tomcat打破了Java中的双亲委派机制。每个应用都有属于自己的类加载器,使得各应用之间进行隔离。Tomcat也是一个Java开发的项目,如果不自定义加载器,那么他的所有类都会使用ApplicationClassLoader去进行加载,那么相当于Tomcat中自己的类与Tomcat中多个应用中的类都会使用JDK中的应用加载器去加载,就没有隔离效果。所以我们需要自定义加载器,并且打破双亲委派机制,去自定义加载顺序,不仅仅是为了各个应用之间的隔离,也是为了与Tomcat中的类文件进行隔离。
Tomcat类加载的源码在java.org.apache.catalina.loader.WebappClassLoader.loadClass方法中。
public synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class> clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// (0) Check our previously loaded local class cache
// 通过Tomcat自己的缓存Map去查找class对象
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.1) Check our previously loaded class cache
// 如果Tomcat自己的缓存没有则去JVM中查找class对象
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding J2SE classes
// 尝试通过Java中的应用加载器去加载,防止webapps下重写了JDK中的类,例如String类等
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (0.5) Permission to access this class when using a SecurityManager
// 如果开启了安全管理则进行权限检查
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
// (1) Delegate to our parent if requested
// 判断是否交给父类加载器去加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = Class.forName(name, false, loader);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
// 通过自己的自定义加载器去加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
// 如果自己的自定义加载器去加载没有加载到,并且没有开启使用父类加载器加载,则还是会尝试使用父类加载器加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = Class.forName(name, false, loader);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
throw new ClassNotFoundException(name);
}
Tomcat在自己加载class文件的时候,是根据文件路径名称在资源节点Map集合中获取到他的类加载器,返回之后再根据获取到的类加载器进行解析。
到这里Tomcat源码的学习就要结束啦,希望这两篇文章对于小伙伴们有所帮助,由于个人能力不足可能过程有些跳,不太连贯,如有疑问还可私聊,十分欢迎。