尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb实战源码跑通与改造:Servlet、Filter、Listener全链路解析

JavaWeb实战源码跑通与改造:Servlet、Filter、Listener全链路解析 简介这份JavaWeb开发实战源码面向希望系统掌握Java Web开发流程的个人学习者尤其适合正在从基础语法向完整项目实践过渡的初学者与进阶开发者。资源以webdemo项目为主线通过分层结构演示MVC设计模式Model层处理业务逻辑与数据库交互View层由JSP页面、CSS样式和JavaScript脚本构成Controller层负责请求接收与视图调度帮助读者理解从页面制作到后端处理的完整链路。压缩包共450个文件约6.75MB包含288个png、36个class、32个gif、22个jsp、18个java、16个js、11个xml及4个jar等覆盖页面素材、源码、配置与依赖便于对照运行与调试。目前已有54人学习。源码还涉及JDBC连接数据库、SQL增删改查以及预编译语句防SQL注入、输入过滤防XSS等安全实践可作为搭建个人项目、优化代码结构与提升可维护性的参考范例。1. JavaWeb开发实战源码从能跑到能改中间隔着多少坑很多人拿到一份 JavaWeb 开发实战源码第一反应是双击打开、找 main 方法、点运行然后浏览器一刷新——404。这不是源码有问题而是 JavaWeb 项目和普通 Java 程序在启动方式上根本不是一回事。它没有 main 方法入口靠的是 Servlet 容器Tomcat加载 web.xml 或注解来驱动整个请求链路。你拿到的源码里Servlet、Filter、Listener、JSP、数据库连接池、前端静态资源这些东西各自有各自的加载时机和依赖顺序任何一个环节对不上项目就是跑不起来。这份实战源码真正能解决的事是让你看到一个完整的请求从浏览器发出到数据库返回的全链路长什么样。它适合两类人一是刚学完 Servlet/JSP 语法、还没把知识串起来的在校生二是工作里只负责某个模块、想补全对 JavaWeb 整体架构认知的后端开发。热搜里常出现的「javaweb项目完整案例mysql」「idea运行javaweb项目配置」说的其实就是同一件事——把源码跑通只是起点能改、能加功能、能排查问题才算真正吃透。2. 把源码跑起来之前先搞清楚它的骨架长什么样2.1 一个典型 JavaWeb 实战项目的目录结构与职责划分拿到源码别急着配 Tomcat先花十分钟把目录结构看明白。一个标准的 Maven 结构 JavaWeb 项目核心目录大致是这样目录/文件职责容易搞混的点src/main/javaServlet、Service、DAO、实体类包名要和 web.xml 里的 servlet-class 对应src/main/resources数据库配置、日志配置、Mapper XML编译后会被放到WEB-INF/classes下src/main/webappJSP、HTML、CSS、JS、WEB-INFwebapp 不是 webapps少个 swebapp/WEB-INF/web.xmlServlet 映射、Filter 配置Servlet 3.0 可以用注解替代webapp/WEB-INF/lib第三方 jar 包Maven 项目里通常为空pom.xml依赖管理、打包方式packaging 必须是 war不是 jar这里最容易翻车的是packaging标签。如果源码的 pom.xml 写的是packagingjar/packaging那它打出来的包 Tomcat 根本不认。JavaWeb 项目必须是 war 包因为 war 有固定的目录规范Tomcat 靠这个规范去找WEB-INF/web.xml和 class 文件。另一个高频问题是 JDK 版本。很多实战源码写的时候用的是 JDK 8你本地装的是 JDK 17 或 21编译能过但运行时报Unsupported class file major version。这不是代码问题是字节码版本不兼容。解决办法要么换 JDK 8要么在 pom.xml 里把maven.compiler.source和maven.compiler.target显式设成 8同时确认 Tomcat 版本支持你用的 JDK。2.2 用 IDEA 配置 Tomcat 并跑通第一个 Servlet 请求假设你已经用 IDEA 打开了项目Maven 依赖也下载完了接下来是配置运行环境。步骤不复杂但每一步都有坑。第一步确认 Facet 配置。打开File → Project Structure → Modules检查有没有 Web Facet。如果没有点添加把Web Resource Directory指向src/main/webapp把Source Roots勾上src/main/java和src/main/resources。这一步不做IDEA 不会把项目识别成 Web 项目后面配置 Tomcat 时部署选项里根本看不到 artifact。第二步配置 Artifact。在Project Structure → Artifacts里添加一个Web Application: Exploded选择From Modules确认WEB-INF/classes指向编译输出目录WEB-INF/lib包含所有 Maven 依赖。Exploded 模式的好处是改 JSP 和静态资源不用重新打包热更新快。第三步配置 Tomcat Run Configuration。点Run → Edit Configurations → → Tomcat Server → Local在Server标签页指定 Tomcat 安装目录在Deployment标签页点添加刚才配的 ArtifactApplication context 设成/或/demo。# 如果你用命令行方式验证 war 包是否正常可以这样操作 # 先打包 mvn clean package -DskipTests # 确认 target 目录下生成了 war 文件 ls target/*.war # 把 war 丢到 Tomcat 的 webapps 目录下独立部署方式 cp target/demo.war $CATALINA_HOME/webapps/ # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # 看日志确认部署成功 tail -f $CATALINA_HOME/logs/catalina.out上面这段命令的逻辑是Maven 打包生成 warTomcat 启动时自动解压 war 并加载应用。-DskipTests是为了跳过测试加快打包速度第一次跑建议加上等确认能跑通再跑测试。catalina.out是 Tomcat 的主日志文件部署失败的原因基本都能在这里找到——比如ClassNotFoundException说明依赖没打进去NoClassDefFoundError说明依赖版本冲突。提示IDEA 里配置的 Tomcat 和独立安装的 Tomcat 是两套东西。IDEA 用的是自己的一份配置副本不会污染你本地的 Tomcat 安装目录。但这也意味着你在 IDEA 里改了 server.xml独立启动的 Tomcat 不受影响。2.3 数据库连接配置从 jdbc.properties 到连接池参数JavaWeb 实战源码里数据库连接配置通常放在src/main/resources下的jdbc.properties或db.properties文件里。内容大概长这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password这里有几个参数必须说清楚。serverTimezoneAsia/Shanghai不加的话MySQL 8 驱动会报时区错误这是最高频的翻车点。useSSLfalse在本地开发时加上避免证书警告。characterEncodingutf8要和数据库建表时的字符集一致否则中文乱码。连接池方面老项目常用 Druid 或 C3P0新一点的用 HikariCP。以 Druid 为例核心参数就几个druid.initialSize5 druid.minIdle5 druid.maxActive20 druid.maxWait60000 druid.validationQuerySELECT 1initialSize是启动时创建的连接数maxActive是最大连接数maxWait是获取连接的超时时间毫秒。validationQuery用来检测连接是否有效MySQL 用SELECT 1就行。这几个参数设小了并发上不去设大了数据库扛不住。本地开发 5 到 20 足够了生产环境要根据实际 QPS 压测后调整。3. 读懂请求链路Servlet、Filter、Listener 的加载顺序与协作方式3.1 一个 HTTP 请求在 JavaWeb 容器里到底经过了哪些组件浏览器发一个请求到 TomcatTomcat 根据端口和 context path 找到对应的 Web 应用然后按 web.xml 或注解的配置走一遍过滤器和 Servlet。顺序是这样的Listener监听器应用启动时最先初始化contextInitialized方法在应用加载时执行用来做全局初始化比如加载数据库驱动、初始化缓存。Filter过滤器请求到达 Servlet 之前执行可以链式调用多个 Filter顺序由 web.xml 里filter-mapping的声明顺序决定。常见用途是编码设置、登录校验、日志记录。Servlet真正处理请求的组件doGet或doPost方法里写业务逻辑。Filter 的后置处理Servlet 处理完后请求会沿 Filter 链反向返回可以在这一步做响应包装。这个顺序不是随便定的。Listener 最早是因为它要在所有组件之前准备好运行环境Filter 在 Servlet 之前是因为它要做请求预处理Servlet 最后是因为它才是业务逻辑的终点。// 一个典型的编码过滤器解决 POST 请求中文乱码 public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig filterConfig) throws ServletException { // 从 web.xml 读取配置的编码格式没配就用默认的 UTF-8 String param filterConfig.getInitParameter(encoding); if (param ! null !param.isEmpty()) { this.encoding param; } } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 设置请求和响应的编码 request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); response.setContentType(text/html;charset encoding); // 放行交给下一个 Filter 或 Servlet chain.doFilter(request, response); } Override public void destroy() { // 过滤器销毁时的清理逻辑一般不用写 } }这段代码的关键在chain.doFilter(request, response)不调用这一行请求就被拦截了后面的 Servlet 永远收不到。init方法里的filterConfig.getInitParameter读取的是 web.xml 里init-param配的值这样编码格式可以不改代码就调整。3.2 Servlet 生命周期与线程安全问题Servlet 的生命周期只有三个阶段init一次、service多次、destroy一次。容器启动时创建 Servlet 实例并调用init之后每个请求来都复用同一个实例调service应用关闭时调destroy。这意味着 Servlet 是单实例多线程的。如果你在 Servlet 里定义了实例变量多个请求同时访问就会出问题。比如public class UserServlet extends HttpServlet { // 危险实例变量被多个线程共享 private ListString userList new ArrayList(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 多个请求同时往 userList 里加数据ArrayList 不是线程安全的 userList.add(req.getParameter(name)); resp.getWriter().write(size: userList.size()); } }这段代码在并发下会出各种玄学问题——数据丢失、size 不对、甚至抛ConcurrentModificationException。正确做法是把变量定义在doGet方法内部变成局部变量每个线程有自己的栈空间天然线程安全。如果确实需要共享数据用ConcurrentHashMap或加锁但更推荐把状态放到数据库或缓存里Servlet 本身保持无状态。3.3 JSP 与 Servlet 的关系编译时机、内置对象与 EL 表达式JSP 本质上就是一个 Servlet。第一次被访问时Tomcat 会把.jsp文件翻译成.java文件再编译成.class文件然后按 Servlet 的方式执行。翻译后的 Java 文件可以在 Tomcat 的work目录下找到路径大概是work/Catalina/localhost/你的应用名/org/apache/jsp/。JSP 有九个内置对象常用的就几个request、response、session、application、out。session用来存用户登录状态application存全局配置。EL 表达式${}用来简化取值比如${user.name}等价于% ((User)request.getAttribute(user)).getName() %但 EL 更简洁且能处理 null。JSP 里最容易踩的坑是中文乱码。pageEncoding和contentType两个属性都要设成 UTF-8% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%只设一个不够。pageEncoding管的是 JSP 文件本身的编码contentType管的是响应给浏览器的编码。两个都设 UTF-8 才能保证从文件读取到浏览器渲染全程不乱码。4. 避坑与排查JavaWeb 实战源码跑不通的五个高频原因4.1 现象404浏览器显示「请求的资源不可用」原因最常见的是 Application context 配错了。比如你在 IDEA 里把 context 设成/demo但浏览器访问的是http://localhost:8080/那肯定 404。另一个原因是 Servlet 的WebServlet(/user)注解和 web.xml 里的url-pattern冲突容器不知道该用哪个。解决先确认访问路径。IDEA 的 Tomcat 配置里Deployment 标签页有一个Application context这个值就是 URL 的前缀。如果设的是/demo访问路径就是http://localhost:8080/demo/user。其次检查 web.xml 的metadata-complete属性如果设成true容器会忽略所有注解只认 web.xml。改成false或删掉这个属性注解才会生效。4.2 现象500报 ClassNotFoundException 或 NoClassDefFoundError原因ClassNotFoundException通常是某个 jar 包没打进WEB-INF/lib。Maven 项目里如果依赖的 scope 写成了provided打包时就不会包含这个 jar。NoClassDefFoundError更隐蔽通常是类加载时静态初始化失败了比如数据库驱动加载了但连接池初始化抛异常。解决先看catalina.out或 IDEA 控制台的完整堆栈找到具体是哪个类找不到。如果是providedscope 的问题检查 pom.xml 里对应依赖的scope标签。如果是静态初始化失败看ExceptionInInitializerError后面的Caused by那里才是真正的根因。4.3 现象数据库连接报 Access denied 或 Communications link failure原因Access denied是用户名密码不对或者该用户没有从当前主机连接的权限。Communications link failure是连不上 MySQL可能是 MySQL 没启动、端口不对、或者防火墙拦了。解决先用命令行验证mysql -u root -p -h localhost -P 3306能不能连上。能连上说明 MySQL 没问题问题在 Java 配置里。检查jdbc.url里的主机名和端口localhost和127.0.0.1在某些环境下行为不一样。如果是权限问题用GRANT ALL PRIVILEGES ON demo_db.* TO root% IDENTIFIED BY password;授权%表示允许从任意主机连接。4.4 现象JSP 页面中文显示为乱码原因三个地方的编码没对齐——JSP 文件本身的编码、响应头的编码、数据库的编码。任何一个环节用了 ISO-8859-1中文就会乱。解决按链路逐个排查。JSP 文件头部确认pageEncodingUTF-8响应确认contentTypetext/html; charsetUTF-8数据库确认建库建表时用了utf8mb4连接 URL 确认带了characterEncodingutf8。四个地方全对齐乱码基本就消失了。4.5 现象修改代码后重启 Tomcat 不生效原因IDEA 默认的 On Update Action 是Restart server但有时候 class 文件没重新编译或者 JSP 的缓存没清。Tomcat 会把翻译后的 JSP 缓存在 work 目录改了 JSP 但缓存没刷新就会看到旧页面。解决在 Run Configuration 的 Server 标签页把On Update action设成Update classes and resourcesOn frame deactivation也设成Update classes and resources。如果还不行手动删掉 Tomcat 的 work 目录再重启。JSP 的修改建议直接重启别依赖热更新省得排查半天发现是缓存问题。5. 从能跑到能改给实战源码加一个自定义 Filter 并验证跑通源码只是第一步真正检验你有没有理解 JavaWeb 的方式是给它加一个功能然后验证。我一般会加一个请求耗时统计 Filter因为它涉及 Filter 的注册、链式调用、日志输出能把前面讲的链路知识全串起来。WebFilter(/*) public class TimingFilter implements Filter { private static final Logger log LoggerFactory.getLogger(TimingFilter.class); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; long start System.currentTimeMillis(); try { // 放行请求让后续 Filter 和 Servlet 处理 chain.doFilter(request, response); } finally { long cost System.currentTimeMillis() - start; // 记录请求方法、URI 和耗时 log.info({} {} cost{}ms, req.getMethod(), req.getRequestURI(), cost); } } }这段代码的关键在finally块。把耗时统计放在finally里不管 Servlet 处理时抛没抛异常耗时都会被记录。如果用WebFilter(/*)注解注册要确认 web.xml 的metadata-complete不是true否则注解不生效。另一种注册方式是在 web.xml 里配filter filter-nametimingFilter/filter-name filter-classcom.demo.filter.TimingFilter/filter-class /filter filter-mapping filter-nametimingFilter/filter-name url-pattern/*/url-pattern /filter-mapping两种方式选一种就行不要同时用否则 Filter 会被注册两次日志打两遍。验证方法很简单启动 Tomcat访问任意页面看控制台有没有输出类似GET /demo/user cost45ms的日志。如果没有先确认 Filter 类有没有被编译到WEB-INF/classes下再确认url-pattern是不是/*。如果日志有输出但耗时明显不对比如每次都是 0ms那可能是System.currentTimeMillis()被调用了两次但中间没有实际处理逻辑检查chain.doFilter有没有被正确调用。进阶一点可以把耗时数据写到响应头里前端用performance.getEntriesByType(navigation)对比服务端耗时和浏览器感知耗时能看出网络传输占了多少时间。这个技巧在排查「接口慢」的问题时特别有用——到底是服务端处理慢还是网络慢数据一对比就清楚了。我自己的习惯是每拿到一份新源码先跑通再加一个 Filter 或 Listener然后故意改错一个配置看报错信息长什么样。这样下次遇到类似问题看日志第一行就能猜到根因。JavaWeb 的东西不复杂但细节多踩过的坑才是自己的。希望帮到你。本文还有配套的精品资源点击获取
返回列表