前置知识

什么是 JSP

JSP (JavaServer Pages),其是 JavaEE 标准规范的组件之一。其主要关于如何动态生成网页
JSP 是一种服务器端的渲染技术,它允许在 HTML 页面中直接嵌入 Java 代码(通常用 <% ... %> 标签包裹)。本质上经过编译后的 JSP 就是一个 Servlet 文件,JSP 只是 Servlet 的一种高级表现形式,我们可以与 PHPASPASP.NET 等类似的脚本语言进行类比

既然说到了 JSP,我认为可以先了解一下 Web 容器的一些基础概念,这是一种用于运行 Java Web 应用程序的环境,支持运行Java ServletJavaServer Pages (JSP) 和其他基于 Java 的 Web 组件

在 Web 应用中,不仅仅只有一个 HTTP 的请求和相应功能,Java Web 容器就还包括:

  • HTTP 请求/响应处理:内置支持 HTTP 协议的解析和通信。
  • 会话管理:提供 Session 和 Cookie 的管理。
  • 生命周期管理:Servlet、Filter 等组件的加载、初始化、销毁等全部由容器负责。
  • 安全性:支持用户身份验证、授权机制、HTTPS 等。
  • 静态资源管理:自动处理静态文件(如 HTML、CSS、JS)的请求。
  • 线程池和资源管理:内置线程池机制,用于高效处理并发请求。

这就是一个运行 Web 应用的完整生态环境,把所有的功能都打包了,容器化的思想来呈现

Servlet

Servlet 是 Java Web 技术中的核心组件,用于处理客户端请求并生成动态内容。它运行在支持 Java 的 Web 容器中,是 Java EE 标准的一部分。

回到 JSP,其之所以能处理请求并生成动态的 HTML 内容,是因为 JDK 会先通过节点树将 JSP 模板文件转换为 Java 源代码,再将其编译成一个 JVM 可识别的 class 文件,从而生成一个 Servlet 类。此时,Servlet 就像其他的 Java Servlet 一样开始执行,具备热更新的能力。本质上是借助了自定义 ClassLoader,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC 回收

源码分析

JSP 的自定义类加载器并不复杂,在源码 org.apache.jasper.servlet.JasperLoader 可以查看,这里我们引入部分 tomcat 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<properties>  
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<tomcat.version>9.0.98</tomcat.version>
</properties>

<dependencies>
<!-- Tomcat 核心引擎源码 (Catalina) -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat Jasper 引擎 (JSP 解析) -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat Coyote (连接器/HTTP处理) -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat API (Servlet/JSP API) -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-api</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency></dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class JasperLoader extends URLClassLoader {  
private final PermissionCollection permissionCollection;
private final SecurityManager securityManager;

public JasperLoader(URL[] urls, ClassLoader parent, PermissionCollection permissionCollection) {
super(urls, parent);
this.permissionCollection = permissionCollection;
this.securityManager = System.getSecurityManager();
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
return this.loadClass(name, false);
}

public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
clazz = this.findLoadedClass(name);
if (clazz != null) {
if (resolve) {
this.resolveClass(clazz);
}

return clazz;
} else {
if (this.securityManager != null) {
int dot = name.lastIndexOf(46);
if (dot >= 0) {
try {
if (!"org.apache.jasper.runtime".equalsIgnoreCase(name.substring(0, dot))) {
this.securityManager.checkPackageAccess(name.substring(0, dot));
}
} catch (SecurityException se) {
String error = "Security Violation, attempt to use Restricted Class: " + name;
se.printStackTrace();
throw new ClassNotFoundException(error);
}
}
}

if (!name.startsWith(Constants.JSP_PACKAGE_NAME + '.')) {
clazz = this.getParent().loadClass(name);
if (resolve) {
this.resolveClass(clazz);
}

return clazz;
} else {
return this.findClass(name);
}
}
}

public InputStream getResourceAsStream(String name) {
InputStream is = this.getParent().getResourceAsStream(name);
if (is == null) {
URL url = this.findResource(name);
if (url != null) {
try {
is = url.openStream();
} catch (IOException var5) {
}
}
}

return is;
}

public final PermissionCollection getPermissions(CodeSource codeSource) {
return this.permissionCollection;
}
}

其核心作用就是加载编译后的 JSP 类,类的全限定名命名规则是 org.apache.jsp.index_jsp(文件名加上 _jsp。路径中的斜杠 / 会变成包名中的点 .
观察可以发现该类继承自 URLClassLoader,这意味着它能够从一组 URL 路径加载类,当然通常是存放 .class 编译文件的目录

其次定义了 JasperLoader 的父类加载器,通常是 WebappClassLoader ,这并不是 JVM 中原生类加载,而是 Tomcat 自定义的,每个 Web 应用完全独立一个,这是为了方便 JSP 访问我们项目的代码

整个父子类关系大概是这样的,了解即可

1
2
3
4
5
6
7
Bootstrap ClassLoader 引导类加载器
Extension ClassLoader 扩展类加载器
App ClassLoader 系统类加载器

Common ClassLoader // 加载 Tomcat 服务器内部和所有 Web 应用公用的库
Webapp ClassLoader
JasperLoader

重点在于重写的 loadClass() 方法,打破了双亲委派模型,先有一个缓存检查,防止同一个类重复加载

而后是一个安全检查这里我们略过,主要看委派逻辑

会检查类名是否以 org.apache.jsp 开头,如果不是,委托给父类加载器,如果是则调用 URLClassLoader.findClass(name) 进行加载

1
public static final String JSP_PACKAGE_NAME = System.getProperty("org.apache.jasper.Constants.JSP_PACKAGE_NAME", "org.apache.jsp");

整个过程不算复杂,我们下个断点来看看

委派时检测到对应的类名

以上只是对 JSP 一个简单的了解,并没有很深入,具体利用需要结合其他场景