前置知识
JSP (JavaServer Pages),其是 JavaEE 标准规范的组件之一。其主要关于如何动态生成网页
JSP 是一种服务器端的渲染技术,它允许在 HTML 页面中直接嵌入 Java 代码(通常用 <% ... %> 标签包裹)。本质上经过编译后的 JSP 就是一个 Servlet 文件,JSP 只是 Servlet 的一种高级表现形式,我们可以与 PHP、ASP、ASP.NET 等类似的脚本语言进行类比
既然说到了 JSP,我认为可以先了解一下 Web 容器的一些基础概念,这是一种用于运行 Java Web 应用程序的环境,支持运行Java Servlet、JavaServer Pages (JSP) 和其他基于 Java 的 Web 组件
在 Web 应用中,不仅仅只有一个 HTTP 的请求和相应功能,Java Web 容器就还包括:
- HTTP 请求/响应处理:内置支持 HTTP 协议的解析和通信。
- 会话管理:提供 Session 和 Cookie 的管理。
- 生命周期管理:Servlet、Filter 等组件的加载、初始化、销毁等全部由容器负责。
- 安全性:支持用户身份验证、授权机制、HTTPS 等。
- 静态资源管理:自动处理静态文件(如 HTML、CSS、JS)的请求。
- 线程池和资源管理:内置线程池机制,用于高效处理并发请求。
这就是一个运行 Web 应用的完整生态环境,把所有的功能都打包了,容器化的思想来呈现
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> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>${tomcat.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>${tomcat.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-coyote</artifactId> <version>${tomcat.version}</version> <scope>provided</scope> </dependency> <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 一个简单的了解,并没有很深入,具体利用需要结合其他场景