ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP项目结构全解析:从目录布局到Tomcat部署与Nginx代理

JSP项目结构全解析:从目录布局到Tomcat部署与Nginx代理 要说JSP项目结构这事儿我还真有一肚子经验可以聊。这些年维护老系统、给实训学员讲JavaWeb十有八九的问题最后都归结到同一个源头——没搞懂JSP项目在磁盘上到底是怎么摆的、运行时又是怎么映射的。每次看到有人搜“idea新建jsp项目”之后对着目录发懵或者问“nginx支持jsp吗”我就知道又是一个被结构绕晕的同学。其实JSP没有想象中神秘它的目录结构和运行机制都是非常规整的一套逻辑只要把这条线捋顺从入门到给老项目打包部署都能顺手很多。这篇就围绕JSP的结构做一次完整拆解。从项目目录该长什么样到IDEA里怎么搭、页面内部那几层结构到底是什么意思再到最终打成war包丢进Tomcat、用Nginx做前置代理时会发生什么顺便把新手最容易踩的坑也一并说了。无论你是刚准备交JavaWeb实训作业的大学生还是接到老系统维护任务被迫碰JSP的开发者这篇文章应该都帮得上忙。1. JSP项目的目录结构先从硬盘上的样子说起1.1 传统JSP项目的标准目录布局先不聊代码怎么写把项目文件夹展开看一眼。传统JSP项目在IDEA里的典型结构是这样project-name/ ├── src/ │ ├── main/ │ │ ├── java/ # 存放Servlet、JavaBean、过滤器等 │ │ ├── resources/ # 配置文件可选 │ │ └── webapp/ # Web应用根目录关键 │ │ ├── WEB-INF/ │ │ │ ├── web.xml # Web应用部署描述符 │ │ │ ├── classes/ # 编译后的.class输出目录 │ │ │ └── lib/ # 第三方jar包 │ │ ├── index.jsp │ │ ├── static/ # css/js/images等静态资源 │ │ └── views/ # 业务页面可选 │ └── ... └── pom.xml或build.gradle部分老项目会稍有出入比如把静态资源直接放webapp根目录下或者把JSP页面层层嵌套在业务目录里这些都算正常。但是有两条铁律是任何JSP项目都不会变的webapp目录是整个Web应用的根以及WEB-INF目录有特殊访问限制。这两条记住了目录结构就算看懂了一半。以前用Eclipse做JavaWeb开发的同学可能更熟悉WebContent这个叫法。说到底都是一回事项目里的某个目录被标记成了Web根目录。IDEA里叫webappEclipse里叫WebContent本质上就是告诉Tomcat“从这层开始外面可以通过URL直接访问里面的文件”。1.2 WEB-INF目录为什么这么重要WEB-INF是整个Web应用里最特殊的一个目录。它能存在release之后的war包里但浏览器通过URL永远访问不到里面的内容。Tomcat的底层会对WEB-INF/**规则做拦截只要请求路径里出现WEB-INF片段直接返回404。既然外面访问不到那放在里面的东西自然就非常安全。典型的操作是把JSP页面放进WEB-INF下然后通过Servlet或Controller转发forward给这些页面。转发是服务端内部行为不经过浏览器那一层用户看到的URL是Servlet的地址却渲染出了JSP的内容既保护了页面文件也把访问入口统一管理起来了。常见的错误做法是把JSP页面裸放在webapp根目录下。虽然这样直接访问http://localhost:8080/app/xxx.jsp就能打开页面看起来方便但意味着任何用户都能猜到或遍历到页面地址直接跳过Servlet处理逻辑去访问JSP。如果JSP里有未做权限判断的代码或者依赖Servlet提前设置的请求属性那访问结果要么报错要么绕过权限控制。所以稍有规模的项目页面全部收进WEB-INF下几乎成了默认规范。lib目录存放这个项目依赖的所有第三方jar包。不同于Maven在构建时帮你拉依赖传统项目是直接把jar文件拷进WEB-INF/lib里打包进war。这两种方式看着不一样但最终部署时classpath的效果是等效的。理解这个区别很重要用Maven管理依赖时构建插件会替你把这些jar从本地仓库搬到WEB-INF/lib里。1.3 前端资源静态文件的放置与访问规则CSS、JS、图片这类静态资源该怎么摆强烈建议在webapp下建一个专门的static目录或叫assets、static叫什么都行别跟WEB-INF混在一起就行。结构大概长这样webapp/ ├── WEB-INF/ # 服务端内容不可直接访问 ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── index.jsp # 首页访问规则非常直白放在webapp下的任何文件——包括子目录里的——浏览器都能通过/项目名/相对路径直接访问。所以static/css/style.css对应的是http://localhost:8080/项目名/static/css/style.css。放到WEB-INF里的JSP页面则必须走Servlet转发。有个细节容易被新手忽略JSP里引用静态资源的路径最好用绝对路径以/开头或者用EL表达式拼接项目上下文路径${pageContext.request.contextPath}。否则你从/index.jsp跳转到/views/user/list.jsp时相对路径css/style.css会解析成/views/user/css/style.css直接404。这个坑几乎每个入行的人都踩过。2. 用IDEA从零搭建一个JSP项目2.1 项目类型选择与Maven配置虽然IDEA内置了Java Enterprise向导但我的建议是老老实实创建Maven项目手动补全Web结构。为什么因为IDEA的向导模板在不同版本中长得完全不一样版本差异大照网上教程走很容易在某个按钮上卡壳。而Maven项目就稳定多了——新建项目时选MavenJDK版本选1.8老项目一般配这个新的能用11或17就用新版然后加一个packaging为war的pom.xml就结束了。核心pom.xml片段packagingwar/packaging dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies注意Servlet和JSP的依赖scope用provided理由非常实际这些jar在Tomcat里已经自带如果以默认scope打进去war包会出现一份与Tomcat重复甚至冲突的类轻则警告重则启动异常。添加完依赖后点一下Maven面板的刷新按钮。等依赖下载好IDEA会自动在src/main下生成webapp目录吗通常不会。你需要自己手动创建src/main/webapp然后在Project Structure快捷键CtrlAltShiftS里把这个目录标记为Web资源目录并新建WEB-INF/web.xml。2.2 手动创建web.xml与第一个JSP页面web.xml是传统JSP项目的部署描述符。Servlet容器的启动流程、Servlet的注册映射、欢迎页、过滤器链等全都由它描述。一个最精简的Servlet 4.0 web.xml长这样?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namejsp-structure-demo/display-name welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app注意web.xml放在webapp/WEB-INF下这个位置不能变。在较新的Servlet 3.0规范里web.xml可以用注解替代WebServlet、WebFilter但放到web.xml里对维护老项目依然是刚需。不过基于JSP的毕设项目我更推荐用WebServlet注解方式做Servlet注册代码紧凑省去在xml里维护URL映射的麻烦。创建index.jsp% page contentTypetext/html;charsetUTF-8 languagejava % html head titleJSP结构示例/title /head body h1欢迎进入JSP结构示例/h1 p当前时间% new java.util.Date() %/p /body /html这个页面已经包含了JSP页面的最基本结构第一行是page指令后面是普通HTML一小段Java表达式。跟写HTML相比JSP就是在HTML里按规则嵌入了Java代码而已。2.3 配置本地Tomcat与部署细节在IDEA里跑JSP项目需要给它配一个本地Tomcat。从右上角运行配置找到Tomcat Server - Local选择本地的Tomcat版本然后在Deployment选项卡里把当前项目添加到部署列表。这里有个容易踩的坑IDEA里Tomcat的Application context默认会带项目名比如/jsp-structure_war访问首页的URL就是http://localhost:8080/jsp-structure_war/。如果你希望URL是根路径/可以在Deployment里把Application context改成/。这个细节很多人忽略导致参照教程的路径去访问却打不开页面。直接把Tomcat的端口改成80不推荐。开发阶段保持8080就行部署阶段端口问题交给运维处理即可。另外修改JSP或Java代码后旧的Tomcat不一定自动热部署。IDEA默认会在运行模式下自动检测并重新部署修改过的页面前提是Exploded方式部署。建议统一使用war exploded模式部署比打war包快了不止一倍。3. JSP页面内部的四层结构拆解3.1 指令Directive的结构与使用一个完整的JSP文件由四类元素交织而成指令Directive、脚本元素Scripting、动作Action和模板文本普通HTML。先从指令说起。指令以%开头%结束用来向JSP容器声明页面级信息。三种指令各有各的作用page指令定义页面属性和导入的类比如contentType控制输出编码errorPage指定异常转发页isELIgnored决定是否解析EL表达式。include指令静态包含其他文件相当于把所有内容原样合并进当前页面再编译。taglib指令引入标签库JSTL标签库也得靠这个指令宣告使用。一个常见疑问是page指令写多行可以吗可以。IDE没有规定一个page指令只能出现一次。甚至% page importjava.util.* %完全可以和% page contentTypetext/html;charsetUTF-8 %分开写。注意page指令里的contentType和pageEncoding一定要加上charset否则中文字符全变乱码。开发中碰到JSP输出乱码问题八成就是漏了contentTypetext/html;charsetUTF-8。3.2 脚本元素Scriptlet、表达式、声明脚本元素是传统JSP让很多人反感的部分但既然聊结构就必须把它说透。Scriptlet小脚本% Java代码 %直接嵌入执行逻辑。表达式Expression% 表达式 %把表达式的计算结果输出到页面。声明Declaration%! 方法或成员变量 %声明JSP页面对应Servlet类的成员。最简单的例子%-- 声明会被翻译成Servlet类的成员方法 --% %! private String formatMsg(String msg) { return [ msg ]; } % %-- Scriptlet在_jspService方法中执行 --% % String name JSP; if (name.length() 0) { out.println(Hello name); } % %-- 表达式输出表达式的值 --% p% formatMsg(示例) %/p理清这三者的编译位置非常关键。声明在Servlet类内部Scriptlet在_jspService方法的方法体里表达式本质上就是out.print(表达式)的简写。如果写代码时混淆了作用范围容易遇到“变量找不到”或“重复定义”这类编译错误。写到这里必须强调哪怕是实训项目页面里也不建议塞大量Scriptlet。从JSP的演进方向和可维护性角度JSP页面应该以展示为主逻辑交给Servlet或JavaBean。这一点随着你接触体量大一点的项目会有很深的体会。3.3 隐式对象与内置作用域JSP页面不需要手动创建就能直接使用的对象叫隐式对象Implicit Object。它们由容器在_jspService方法里预先定义好默认有九个request、response、out、session、application、pageContext、config、page、exception。理解隐式对象核心在于理解它们的作用域Scope。JSP有四个作用域从小到大依次是pageScope仅当前JSP页面有效用完即焚。requestScope同一次请求中有效Servlet转发给JSP也能访问。sessionScope同一个用户会话内有效。applicationScope整个应用全局共享直到容器关闭。这四者的区别用生活化类比解释就是pageScope是计算器上临时记的一个数关机就没了requestScope是这张订单上写的信息只对本笔订单有意义sessionScope是你在这家店办的会员卡只要还在店里会话未超时就能用applicationScope是店门口的价目表所有顾客都看同一个版本。实操里最常用的配合方式是Servlet在请求里request.setAttribute(user, userObj)然后request.getRequestDispatcher(/WEB-INF/views/userInfo.jsp).forward(request, response)在JSP里用EL表达式${user.name}取出来。这套配合干净利落也是整个JSPServlet架构最核心的链路。3.4 JSTL与EL表达式——现代JSP结构的正确姿势论JSP里怎么写循环和判断最规范的做法就是用JSTL标签库配合EL表达式把Java代码从页面里彻底剥离出去。页面结构会变成% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % ... table c:forEach items${userList} varitem tr td${item.id}/td td${item.name}/td td${item.email}/td /tr /c:forEach /tableEL表达式${}负责取值JSTL标签负责流程控制。这一步跟我前面说的“JSP以展示为主”一脉相承。如果你去看那些还能跑得很稳的老项目会发现好一点的代码库基本都遵守这个规范。有一个经验分享给你能不用Scriptlet就尽量别用。每次修改JSP页面里的Java代码都会被容器重新编译脚本错误在页面上报错很不好看但标签库和EL出问题通常是空白页或属性找不到排查方向更清晰。页面里大段Java代码不仅阅读体验极差还极易跟HTML嵌套语法撞车。4. 从开发到部署JSP结构在打包与服务器端的体现4.1 传统JSP项目打包WAR与部署流程开发完毕之后的事情就是把项目变成可部署的war包。在Maven项目里一条命令就能完成mvn clean package跑完后会在target/目录下生成项目名.war。把这个war包扔到Tomcat的webapps/目录下启动Tomcat它会自动解压并部署。这就是传统JSP项目的部署流程。war包本质上就是一个zip文件把webapp目录的所有内容按规则压缩进去xxx.war/ ├── WEB-INF/ │ ├── web.xml │ ├── classes/ # 编译后的class文件 │ └── lib/ # 依赖jar包 ├── static/ └── index.jsp手工打包也不是不行——用jar cvf命令或者压缩软件把符合条件的文件压缩成zip后改名.war。但我不建议手工操作用Maven的好处是它能自动完成编译、拷贝资源、打包lib这整条流水线。有一次我见过有人手工打包时忘了把classes文件夹打进去Web应用启动后疯狂报ClassNotFoundException查了半天才发现是包少了目录纯属给自己挖坑。bear in mind部署Tomcat用的war包目录名决定了应用访问路径。比如把war包改名为ROOT.war会部署为根路径访问http://localhost:8080/直接命中首页保持xxx.war的名字访问路径就是/xxx/。4.2 Nginx能否直接支持JSP该怎么配关于“nginx支持jsp吗”这个问题准确答案非常明确Nginx本身不支持JSP。Nginx是静态Web服务器和反向代理服务器它擅长的是处理HTML、CSS、JS、图片这类静态资源以及把动态请求转发给后端应用。JSP是Java服务端动态页面技术它需要由Tomcat这类Servlet容器来编译执行Nginx没有这个能力。实际部署中的常见架构是把Nginx放在前端充当反向代理和负载均衡用户请求 - Nginx - TomcatJSPNginx配置片段大概长这样server { listen 80; server_name example.com; # 静态资源由Nginx直接返回 location /static/ { alias /usr/share/nginx/html/static/; expires 7d; } # 动态请求转发给Tomcat location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里把JSP请求全部转发给Tomcat处理静态资源由Nginx直接返回既减轻Tomcat压力又提高了静态资源响应速度。理解这个架构后就不再会觉得“Nginx支持JSP吗”是个玄学问题——支持与否取决于你如何编排服务。4.3 与Spring Boot项目目录结构的对比近几年来新项目大多是Spring Boot风格很多新同学从Spring Boot开始接触JavaWeb对传统JSP项目的目录结构感觉陌生。两者对比看结构更好理解维度传统JSP项目Spring Boot项目页面文件src/main/webapp/WEB-INF/views/*.jspsrc/main/resources/templates/*.htmlThymeleaf模板页面技术JSPJSTLThymeleaf/Freemarker根目录webappclasspath:static templates打包产物war需要Tomcatjar内嵌Tomcat可直接java -jar运行部署方式丢进Tomcat的webapps独立进程或nginx反代Spring Boot也不是完全抛弃JSP只要改了pom里的打包方式和依赖它也能支持JSP比如设置packagingwar、引入tomcat-embed-jasper再把JSP放到src/main/webapp下。网上有些老系统迁移就是采用这个方案过渡的。只是Spring Boot官方推荐模板引擎JSP在新项目里逐渐退场是大势所趋。但老实讲只要企业还有老系统在用JSP懂这套结构的人就不愁找不到活儿。5. JSP结构中那些高频问题排查和避坑经验5.1 实战中遇过的经典问题速查表问题现象根本原因解决办法页面能访问但中文乱码缺少pageEncoding或contentType编码配置统一三个地方编码JSP页面charsetUTF-8、web.xml过滤器、Tomcat连接URIEncoding访问/WEB-INF下页面返回404Tomcat防护规则禁止外部直接访问用Servlet或Controller转发不要直接在浏览器地址栏敲路径图片/JS加载404相对路径在前端路由下解析出错用${pageContext.request.contextPath}拼绝对路径本地运行跑起来后改代码没生效IDEA部署模式或热部署未正确配置使用war exploded部署改完触发重新编译打包后启动报ClassNotFoundException依赖jar没打进WEB-INF/libMaven确认依赖scope纯净打war包验证报“The superclass javax.servlet.http.HttpServlet was not found”项目未引入Servlet API依赖在pom.xml添加provided scope的servlet-api依赖5.2 针对JSP结构的几条独家实操建议第一建目录之前先想清楚页面归属。我见过很多人把所有JSP堆在webapp下文件多了之后不仅命名混乱而且互相include时路径绕来绕去。建议按模块划分子目录WEB-INF/views/user/、WEB-INF/views/order/、WEB-INF/views/system/一眼就能知道这个页面属于哪个业务块。第二过滤器编码问题要在web.xml里配好。不管JSP页面写的多么规范如果没有在web.xml里配置CharacterEncodingFilterPOST请求携带的参数中文照样乱码。这段配置是无数JSP项目的标准起手式filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping第三老项目的Java版本和Tomcat版本要匹配。JDK8通常配合Tomcat 8.5或9.0使用JDK11以上配合Tomcat10时需要注意包名变化javax.变成了jakarta.直接把老项目扔给新版TomcatJSP会因包名不匹配起不来。很多老系统升级后遇到启动报错十有八九跟这个版本适配有关。第四没事多看一眼Tomcat日志里JSP编译的报错。JSP页面第一次访问时才编译报错信息往往隐藏在tomcat/logs/localhost.yyyy-MM-dd.log里不是启动错误也不是浏览器500白页能全覆盖的。学会翻日志排错效率能高一半。这几个建议看上去不起眼但是每一个背后都有实打实踩坑的教训。JSP这个技术确实不算新了但理解它的一整套结构逻辑依然能帮你在处理老项目、理解JavaWeb全貌、甚至在面试聊架构演进时都有底气说出个一二三来。我在自己带人的时候最喜欢说的一句话就是别嫌项目老老项目里的结构经验往往是最扎实的。把这篇里面的思路吃透不管是做毕设还是接手旧系统起码不至于在目录和路径上栽跟头。
返回列表