模块化架构之tomcat的jsp加载处理

时间:2022-10-11 09:06:27

最近一直在思考模块化项目的架构问题,也学过一点点OSGI的东西,感觉有点高大上,自身目前还用不上,或者用的不够好。就想着能不能实现一个简单一点的模块化方式,于是便有了前一篇的阅读spring源码,解决了实体实例化上的模块化问题,下面我们来看看资源模块化的问题,主要是jsp。我们都知道,对于服务器(tomcat)来说,要求jsp都要在项目目录下,那如果我们要把jsp封装到相对应的模块jar包中呢,tomcat还能加载吗?

一开始,笔者只是简单的做了读取操作,对于jsp的请求都拦截了读取内容返回前台,结果就出现了很喜感的一幕,jsp根本就不能好好执行,这时笔者才想起来,jsp本质上是一个变种servlet,而不是一个单纯的html页面。于是我们又开始了阅读源码的过程。

通过阅读我们发现,tomcat处理jsp的关键是其默认配置了一个JSPServlet类,用于处理所有的jsp文件。通过分析Service方法和serviceJspFile方法我们发现了tomcat对于jsp的处理基本过程是:

首先,通过uri获取要处理的jsp路径。

其次,尝试从JspRuntimeContext缓存容器中获取JspWrapper如果没有,就开启线程同步去构建一个,构建时会通过context.getResource来判断是否有jsp文件(也就是该方法限定了jsp必须在项目目录下,也就是配置的docBase目录下)。

第三,如果有,就创建JSPWrapper并以uri为key加入缓存中。

第四,调用JspWrapper.service方法来完成jsp响应(该方法完成了jsp的编译存储和服务处理。每次都会判断是不是开发模式和最后一次修改时间,如果有更新就重新编译)

在阅读中我们发现,对于jsp资源获取的关键类实际上只有3个一个是JSPServlet,一个是JspWrapper一个是JspCompilationContext.这三个类。

JSPServlet说了,是用于处理*.jsp的请求,负责获取jsp的路径,并查找相关的JSPWrapper来响应。

JSPWrapper,包装了jsp文件,完成了jsp的编译,和响应

JspCompilationContext,Jsp的编辑容器,完成了jsp的资源查找和编辑器创建等操作,在jsp第一次加载的时候完成了大量的初始化工作。


下面我们就改造这三个类来实现我们自己的想法,这里我继承这三个类分别作了ModuleJspServlet,ModuleJSPWrapper和ModuleJSPCompilationContext类,来完成相关的扩展功能。

ModuleJspServlet中我们简单修改了,判断方法,也就是不在简单的使用ServletContext.getResource来判断是否存在jsp,而是通过JspCompilationContext来判断。


 private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
Throwable exception, boolean precompile)
throws ServletException, IOException {

JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
<span style="color:#ff0000;"> ModuleJspCompilationContext jc = new ModuleJspCompilationContext(jspUri, false, options, context, null, rctxt);</span>
if (wrapper == null) {
synchronized(this) {
wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
// Check if the requested JSP page exists, to avoid
// creating unnecessary directories and files.
if (<span style="color:#ff0000;">null == jc.getResource(jspUri)</span>) {
handleMissingResource(request, response, jspUri);
return;
}
boolean isErrorPage = exception != null;
wrapper = new ModuleJspServletWrapper(config, options, jspUri,
isErrorPage, rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}

try {
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}

}
红色部分使我们的修改。这个类的修改比较简单。

对于ModuleJSPWrapper我们也只是修改了其构造函数:

public ModuleJspServletWrapper(ServletConfig config, Options options,
String jspUri, boolean isErrorPage, JspRuntimeContext rctxt)
throws JasperException {
super(config, options, jspUri, isErrorPage, rctxt);
this.isTagFile = false;
this.config = config;
this.options = options;
<span style="color:#ff0000;">if(jspUri.startsWith("jar:file:")) {
jarPath = jspUri.substring(0,jspUri.indexOf("!/") + 2);
jspUri = jspUri.substring(jspUri.indexOf("!/") + 1);
}else if(jspUri.startsWith("file:")) {//文件系统开始,以此开头处理的是开发模式下
jarPath = jspUri.substring(0,jspUri.indexOf("/") + 1);
jspUri = jspUri.substring(jspUri.indexOf("/"));
}</span>
this.jspUri = jspUri;
ctxt = new ModuleJspCompilationContext(jspUri, isErrorPage, options,
config.getServletContext(), this, rctxt);
}
这里增加了对于jar包文件和工作空间的处理,后者是针对开发的便利性提出的开发模式来的,这样在完成开发前都可以不用打包。

主要的修改在ModulJspCompilationContext中,获取资源的方法我们做了下简单修改。

 public URL getResource(String res) throws MalformedURLException {
URL result = null;
if (res.startsWith("/META-INF/")) {
// This is a tag file packaged in a jar that is being compiled
URL jarUrl = tagFileJarUrls.get(res);
if (jarUrl == null) {
jarUrl = tagFileJarUrl;
}
if (jarUrl != null) {
result = new URL(jarUrl.toExternalForm() + res.substring(1));
}
} else if (res.startsWith("jar:file:") || res.startsWith("file:")) {
// This is a tag file packaged in a jar that is being checked
// for a dependency
result = new URL(res);

<span style="color:#ff0000;"> } else if (this.jarPath != null){
result = new URL(jarPath + res);</span>
}else {
result = context.getResource(canonicalURI(res));
}
return result;
}


我们针对自己的路径做了下处理。这样就完成了jsp的跨项目加载和模块化加载。


总结:这里通过跟踪分析,知识修改了jsp的加载方式,不在只是从项目目录下加载。其他未做修改。

在测试时发现,我们单纯的返回资源不设置请求响应码,容易造成浏览器莫名其妙的缓存问题。这点算是额外收获了。