ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JavaWeb实战源码:从Servlet生命周期到Spring事务避坑指南

JavaWeb实战源码:从Servlet生命周期到Spring事务避坑指南 简介这是一份面向Java Web初学者与进阶学习者的实战型源码资源聚焦MVC分层开发实践帮助学习者系统掌握从页面交互、Servlet控制、JSP视图渲染到数据库CRUD的完整Web应用开发链路。资源压缩包共450个文件大小6.75MB包含22个JSP页面实现View层、18个Java类含UserManageServlet、ValidateCodeServlet等Controller与DAO组件、36个class字节码文件、16个JS脚本增强前端交互、11个XML配置文件如web.xml与框架配置以及大量PNG/GIF图片资源用于界面素材与流程示意图。已有54人学习下载体现了小众但精准的学习需求。源码以webdemo项目为载体清晰呈现Model层业务逻辑与JDBC数据操作、View层JSPCSSJS协同渲染、Controller层请求调度与过滤器如UserLoginFilter安全控制的典型结构特别适合通过代码反向推导原理、理解Struts/Spring/Hibernate等框架集成基础并积累SQL注入与XSS防护等安全编码实践经验。1. JavaWeb开发实战源码不是“抄完就能跑”的压缩包而是你缺的那套可调试、可拆解、可复用的工程化肌肉记忆很多人点开一个标着“JavaWeb开发实战源码”的压缩包双击解压打开IDEA右键 run看到控制台刷出Tomcat started on port(s): 8080就以为“学会了”。结果一换数据库连接404一改JSP路径500一加个Filter整个登录流程就断在半路——不是代码错了是根本没搞清这个“源码”背后到底封装了多少隐性契约Servlet容器生命周期怎么和Spring Bean联动JDBC连接池的close()调用时机为什么总在事务提交后才生效JSP里的EL表达式到底是被哪个类解析的这些不是面试题是每天改需求时真实卡住你的黑匣子。这本《JavaWeb开发实战源码》不是教学视频的配套附件而是一套按企业级项目节奏组织的、带完整调试断点和日志埋点的可运行工程。它覆盖从传统ServletJSPMySQL三层架构到Spring MVCMyBatisDruidLogback的轻量整合再到Spring Boot 2.7.x Thymeleaf HikariCP的现代演进路径。适合两类人刚学完Servlet API但写不出完整登录注册流程的在校生以及能写Spring Boot但一碰Filter链或自定义ViewResolver就发懵的转岗开发者。它不教“怎么配Tomcat”而是让你亲手把web.xml里那行filter-mapping拖进Debug模式单步看到它如何被ApplicationFilterChain组装、执行、传递request——这才是“实战”的本义代码可停、可查、可改、可证伪。2. 用标准Maven结构跑通第一个ServletJSP项目从零建工程、配web.xml到验证请求流转2.1 创建符合Servlet 4.0规范的Maven WebApp骨架JavaWeb项目不是靠“新建Module→选Web Application”点出来的。IDEA默认生成的WebApp模板往往缺web.xml声明、src/main/webapp/WEB-INF/web.xml路径错位、甚至pom.xml里没声明war打包类型。正确做法是手动初始化一个标准结构!-- pom.xml -- packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target servlet.version4.0.1/servlet.version /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version${servlet.version}/version scopeprovided/scope /dependency /dependencies提示scopeprovided/scope是关键。它告诉Maven这个jar由Servlet容器如Tomcat提供编译时需要但打包进WAR时不能包含——否则会和Tomcat自带的servlet-api.jar冲突导致java.lang.LinkageError: loader constraint violation。接着手动创建目录结构src/main/java/com/example/web/ src/main/resources/ src/main/webapp/WEB-INF/web.xml src/main/webapp/index.jspsrc/main/webapp是Web资源根目录WEB-INF必须全大写且严格位于其下这是Servlet规范硬性要求。任何小写web-inf或放在resources里都会让Tomcat启动失败并静默忽略配置。2.2 写一个能被web.xml精准映射的HelloServlet别用WebServlet注解起步——它掩盖了容器初始化的真实顺序。先写传统XML方式才能看清servlet和servlet-mapping的绑定逻辑// src/main/java/com/example/web/HelloServlet.java package com.example.web; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class HelloServlet extends HttpServlet { Override public void init() throws ServletException { System.out.println(【HelloServlet】init() called —— Servlet实例化后立即执行); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(【HelloServlet】doGet() triggered —— 此时request已封装完成); req.setAttribute(message, Hello from Servlet!); req.getRequestDispatcher(/hello.jsp).forward(req, resp); } }注意两点init()方法会在Servlet第一次被请求时调用懒加载不是应用启动时。这是很多初学者误以为“所有Servlet都随Tomcat启动”的根源forward()是服务器端跳转URL栏不变且request对象全程复用——所以setAttribute()设置的属性能在JSP中通过${message}取到。若用sendRedirect()则是客户端重定向会丢失所有request属性。2.3 配置web.xml实现精确路由与生命周期控制WEB-INF/web.xml是JavaWeb的“宪法文件”必须手写且严格遵循DTD/XSD?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 servlet servlet-nameHelloServlet/servlet-name servlet-classcom.example.web.HelloServlet/servlet-class load-on-startup1/load-on-startup !-- 值越小优先级越高 -- /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-appload-on-startup1/load-on-startup让Servlet在Tomcat启动时就初始化而非首次请求时。这对需要预热缓存、建立数据库连接池的Servlet至关重要。值为0或负数则表示按需加载多个Servlet时数字越小越早加载。url-pattern/hello/url-pattern匹配规则有三类精确匹配/hello→ 只响应/hello路径匹配/hello/*→ 响应/hello/a,/hello/b/c扩展名匹配*.jsp→ 响应所有.jsp文件由JspServlet处理。切记路径匹配优先级高于扩展名匹配。若同时存在/hello/*和*.jsp访问/hello/test.jsp会走前者而非JSP引擎——这是404的常见原因。2.4 验证请求流转用浏览器ConsoleDebug三线并行抓包启动Tomcat后在浏览器访问http://localhost:8080/hello观察控制台输出【HelloServlet】init() called —— Servlet实例化后立即执行 【HelloServlet】doGet() triggered —— 此时request已封装完成再在IDEA中对doGet()方法第一行打上断点刷新页面你会看到线程停在req.setAttribute(...)前。此时展开Debug窗口的Variables面板展开req对象能看到requestURI/hello、contextPath空、servletPath/hello——这说明请求确实被url-pattern/hello/url-pattern捕获且未经过任何Filter。参数说明requestURI是完整路径含ContextPathservletPath是匹配到的Servlet路径部分。若部署路径为/myapp访问/myapp/hello则requestURI/myapp/helloservletPath/hello。这是做RESTful路由时判断路径层级的关键依据。3. 从JSP到Thymeleaf为什么模板引擎切换不是改后缀那么简单3.1 JSP的隐式对象与EL表达式失效的三大场景JSP里写${user.name}很自然但新手常遇到“明明request.setAttribute(user, u)了JSP里却显示空白”。这不是EL语法错而是三个隐藏开关没打开page指令未启用ELJSP默认开启EL但若web.xml声明的是Servlet 2.3或更低版本EL会被禁用。检查web.xml的version4.0是否生效isELIgnoredtrue显式关闭在JSP顶部加% page isELIgnoredtrue %会全局禁用EL删掉即可对象属性无getter方法User类必须有getName()不能只有name字段。JSP通过JavaBean规范反射调用getter字段直访不支持。更隐蔽的问题是JSTL标签库未正确引入。想用c:forEach遍历List必须在JSP顶部声明% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %且pom.xml中添加依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency血泪经验JSTL的uri必须和web.xml中声明的taglib完全一致。网上很多教程写成http://java.sun.com/jstl/core少/jsp会导致org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved——这是JSP编译期错误不会出现在控制台日志里只在浏览器报500。3.2 Thymeleaf替代JSP不是语法替换而是渲染时机重构把JSP换成Thymeleaf绝不是把.jsp改成.html再加几个th:text。核心差异在于JSP是服务端模板在Servlet容器内编译执行Thymeleaf是服务端客户端双模模板其th:*属性在服务端渲染后会被移除HTML仍可被浏览器直接打开——这要求你彻底放弃% %式脚本片段转向纯属性驱动。以登录表单为例JSP写法form actionlogin methodpost input typetext nameusername value% request.getParameter(username) ! null ? request.getParameter(username) : % input typepassword namepassword input typesubmit valueLogin /formThymeleaf等效写法form th:action{/login} th:methodpost input typetext nameusername th:value${param.username ?: } input typepassword namepassword input typesubmit valueLogin /form关键变化th:action{/login}{}是URL重写语法自动添加ContextPath避免硬编码/myapp/loginth:value${param.username ?: }param是Thymeleaf内置对象等价于request.getParameter()?:是Elvis操作符防NPE没有% %脚本块Thymeleaf禁止Java代码嵌入强制逻辑与视图分离。3.3 在Spring Boot中集成Thymeleaf自动配置背后的三个BeanSpring Boot的spring-boot-starter-thymeleaf看似一键集成实则暗藏三个关键Bean的自动装配Bean名称类型作用可调参数TemplateResolverITemplateResolver解析模板位置、编码、缓存策略spring.thymeleaf.cachefalse开发关缓存TemplateEngineTemplateEngine执行模板渲染的核心引擎spring.thymeleaf.enabledtrue默认trueThymeleafViewResolverViewResolver将逻辑视图名如login映射为物理路径如/templates/login.htmlspring.thymeleaf.prefixclasspath:/templates/验证是否生效在application.properties中加一行logging.level.org.thymeleafDEBUG启动时若看到[main] DEBUG org.thymeleaf.TemplateEngine - Template login was not found in template cache说明TemplateResolver已工作正在尝试加载/templates/login.html。避坑Thymeleaf默认前缀是classpath:/templates/后缀是.html。若你把HTML文件放在src/main/resources/templates/没问题但若放在src/main/webapp/WEB-INF/templates/则必须显式配置spring.thymeleaf.prefixfile:src/main/webapp/WEB-INF/templates/因为file:协议指向磁盘路径而classpath:只扫描jar包和resources目录。4. JavaWeb项目中的数据库层实战MyBatis配置、SQL注入防护与事务边界4.1 MyBatis核心配置文件mybatis-config.xml的最小必要项MyBatis不是“加个依赖就能用”它需要明确告诉框架SQL写在哪、POJO类在哪、事务怎么管。mybatis-config.xml是它的宪法?xml version1.0 encodingUTF-8? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration typeAliases package namecom.example.entity/ /typeAliases environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ !-- 使用JDBC事务非Spring管理 -- dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/javaweb?useSSLfalseamp;serverTimezoneUTC/ property nameusername valueroot/ property namepassword value123456/ /dataSource /environment /environments mappers mapper resourcecom/example/mapper/UserMapper.xml/ /mappers /configuration重点解析typeAliases为com.example.entity.User注册别名User使Mapper XML中可直接写resultTypeUser无需全限定名transactionManager typeJDBC/表示事务由JDBC Connection控制conn.commit()/conn.rollback()生效。若后续接入Spring则改为typeMANAGED交由Spring的DataSourceTransactionManager接管dataSource typePOOLED使用MyBatis内置连接池非HikariCP适合学习生产环境必须换dataSource typeUNPOOLED配合Druid/HikariCPmapper resource...指定Mapper XML文件路径必须与Java类路径一致。若UserMapper.xml在src/main/resources/com/example/mapper/此处写com/example/mapper/UserMapper.xml若在src/main/java/com/example/mapper/则需确保该目录被标记为Resources Root否则编译后XML不会复制到classes目录。4.2 Mapper XML中#{}与${}的本质区别SQL注入的生死线MyBatis的#{}和${}看起来都是占位符但底层机制天壤之别#{username}→ 被解析为?走PreparedStatement.setString(1, username)参数经JDBC驱动转义绝对安全${username}→ 直接字符串拼接SELECT * FROM user WHERE name admin -- 这种注入语句会原样执行。看一个翻车案例!-- 危险动态表名不能用#{} -- select idselectByTable resultTypeUser SELECT * FROM ${tableName} WHERE id #{id} /select这里${tableName}是必须的因为表名不能参数化。但若tableName来自用户输入就构成严重漏洞。正确做法是白名单校验public ListUser selectByTable(String tableName, Long id) { // 白名单校验表名 ListString allowedTables Arrays.asList(user, order, product); if (!allowedTables.contains(tableName)) { throw new IllegalArgumentException(Invalid table name: tableName); } return userMapper.selectByTable(tableName, id); }再看一个更隐蔽的坑!-- 错误ORDER BY 后不能用#{} -- select idselectAll resultTypeUser SELECT * FROM user ORDER BY #{sortBy} ASC /select#{sortBy}会被转成?而ORDER BY ?是SQL语法错误。必须用${sortBy}但要加白名单String[] allowedSortFields {id, name, create_time}; if (Arrays.asList(allowedSortFields).contains(sortBy)) { return userMapper.selectAll(sortBy); } else { throw new IllegalArgumentException(Invalid sort field); }4.3 Spring事务管理的三大陷阱Transactional为何不生效在Spring Boot中加Transactional是最常见的事务写法但90%的失效源于以下三点自调用失效Service A的methodA()调用本类的methodB()即使methodB()加了Transactional事务也不生效。因为Spring事务基于代理Proxy自调用绕过代理直接走this引用。Service public class UserService { public void methodA() { methodB(); // ❌ 绕过代理事务不生效 } Transactional public void methodB() { ... } // ✅ 但这里不会被代理拦截 }异常类型不对Transactional默认只对RuntimeException及其子类回滚。若方法抛出IOException事务不会回滚。必须显式声明Transactional(rollbackFor Exception.class) public void updateUser() throws IOException { ... }传播行为误解Transactional(propagation Propagation.REQUIRED)默认表示“有事务则加入无则新建”。但若Service A调用Service BB的事务属性是REQUIRES_NEW则A的事务会被挂起B执行完再恢复A——这可能导致A的DB操作在B的事务提交后才写入违反一致性。排查技巧在application.properties中开启事务日志logging.level.org.springframework.transaction.interceptorTRACE logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManagerDEBUG启动后若看到Creating new transaction with name [com.example.service.UserService.updateUser]说明事务已创建若只有Getting transaction for [com.example.service.UserService.updateUser]则说明加入了现有事务。5. JavaWeb项目部署与调试避坑指南从IDEA配置到Tomcat日志定位5.1 IDEA中正确配置Tomcat Server的五个致命细节IDEA的Tomcat配置界面看似简单但以下五处填错会导致项目启动即失败Application context上下文路径默认是/但若你项目名叫javaweb-demo实际访问路径是http://localhost:8080/javaweb-demo/。若在此处填/则IDEA会把项目部署到ROOT覆盖默认欢迎页若填/myapp则必须访问http://localhost:8080/myapp/。建议留空让IDEA自动取模块名。Deployment的Artifact选择必须选中javaweb-demo:war exploded带exploded而非javaweb-demo:war。前者是解压部署修改JSP/HTML可热更新后者是打包部署每次改完都要重新Build Artifact。Server Options的On ‘Update’ action设为Update classes and resources这样CtrlF10热更新时Java类和静态资源都会同步。若选Redeploy会重启整个ContextSession丢失。Startup/Connection的JDK配置Tomcat启动JVM的JDK必须与项目Module SDK一致。若项目用JDK 1.8而Tomcat配置里选了JDK 11会出现Unsupported major.minor version 52.0JDK 1.8字节码版本是52。Configuration的Before launch必须勾选Build project否则代码变更不会编译进out/production/目录导致Tomcat运行旧class。若用Maven还需加Run Maven Goal→compile。5.2 Tomcat日志体系精读catalina.out、localhost.log与access_log的区别Tomcat日志不是一堆乱码每类日志解决不同问题日志文件生成位置记录内容排查场景catalina.out$CATALINA_HOME/logs/Tomcat启动/停止日志、System.out/System.err输出启动失败、OutOfMemoryError堆栈localhost.date.log$CATALINA_HOME/logs/当前Hostlocalhost下所有WebApp的ServletContext.log()、ServletConfig.getServletContext().log()输出Servlet初始化异常、Filter链中断localhost_access_log.date.txt$CATALINA_HOME/logs/HTTP访问日志格式类似Apache127.0.0.1 - - [10/Jan/2024:10:23:45 0800] GET /hello HTTP/1.1 200 123请求404/500、流量分析、慢请求定位关键技巧在Servlet中记录关键路径日志必须用getServletContext().log()而非System.out.println()protected void doGet(HttpServletRequest req, HttpServletResponse resp) { getServletContext().log(【HelloServlet】start processing request); // ✅ 写入localhost.log System.out.println(This goes to catalina.out); // ❌ 不可控且可能被吞 }5.3 常见问题排查404、500、400错误的三层定位法现象访问/hello返回404第一层网络层curl -v http://localhost:8080/hello看HTTP状态码是否真为404排除浏览器缓存第二层容器层查localhost.date.log搜索hello看是否有Mapping servlet [HelloServlet] to [/hello]字样。若无说明web.xml未加载或servlet-mapping写错第三层应用层在HelloServlet的init()方法首行加getServletContext().log(HelloServlet initialized)若localhost.log中无此日志说明Servlet未注册成功检查servlet-class全限定名是否拼错。现象JSP中EL表达式${user.name}显示空白第一层确认user对象已setAttribute在Servlet中System.out.println(req.getAttribute(user))第二层检查JSP顶部是否有% page isELIgnoredtrue %删掉第三层确认User类有getName()方法且name字段是privateJavaBean规范要求getter/setter。现象表单提交后中文乱码如“张三”变“å¼ ä¸‰”第一层在Servlet中System.out.println(new String(req.getParameter(username).getBytes(ISO-8859-1), UTF-8))若能正确打印说明是编码问题第二层在web.xml中加Filterfilter filter-nameCharacterEncodingFilter/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-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping第三层确认HTML表单指定了accept-charsetUTF-8form accept-charsetUTF-8 methodpost action/login避坑总结404不是路径错是映射没生效90%的404源于web.xml未被Tomcat读取检查web.xml是否在WEB-INF/下且web.xml开头的xsi:schemaLocationURL能否访问500不是代码错是类加载失败java.lang.ClassNotFoundException最常见检查pom.xml依赖是否scopeprovided/scope误用或WEB-INF/lib/下jar包是否缺失400不是前端错是参数解析失败HTTP Status 400 – Bad Request多因RequestParam要求必填但前端未传或日期格式不匹配如DateTimeFormat(patternyyyy-MM-dd)但传了2024/01/10。6. 从源码到生产三个让JavaWeb项目真正落地的关键习惯6.1 把web.xml当作API契约文档来维护很多团队把web.xml当成一次性配置文件改完就扔。但它是整个Web应用的入口契约必须像接口文档一样管理。我坚持三个动作用XML注释标注每个配置的业务含义!-- 【登录认证Filter】拦截所有 /admin/** 请求校验Session中user是否为空 -- filter-mapping filter-nameAuthFilter/filter-name url-pattern/admin/*/url-pattern /filter-mapping这比写Wiki文档更及时因为代码和注释永远在一起。用display-name统一命名应用display-nameJavaWeb-User-Management-v1.2/display-name在Tomcat Manager页面一眼识别版本避免测试环境部署错包。把error-page当监控探针error-page error-code500/error-code location/error/500.jsp/location /error-page error-page exception-typejava.lang.Exception/exception-type location/error/global.jsp/location /error-page在/error/global.jsp中记录exception.getMessage()和exception.getStackTrace()到日志文件形成线上异常基线。6.2 数据库脚本版本化用Liquibase替代手工SQL项目初期用mysql -u root -p init.sql很爽但迭代十次后没人记得v3.2.sql改了哪张表。我强制团队用Liquibase在pom.xml加依赖dependency groupIdorg.liquibase/groupId artifactIdliquibase-core/artifactId /dependency创建src/main/resources/db/changelog/db.changelog-master.yamldatabaseChangeLog: - include: file: db/changelog/v1.0/create_user_table.yaml - include: file: db/changelog/v1.1/add_index_to_username.yaml每次建表/加字段都写一个独立的vX.X/change.yamlLiquibase自动记录DATABASECHANGELOG表保证每次mvn compile都精准执行未执行的变更。好处新同事拉代码后mvn clean compile自动建库建表不用问“init.sql在哪”“要先跑哪些脚本”。6.3 日志分级与异步化让debug日志不拖垮生产环境本地开发时log.debug(SQL: {}, sql)很爽但生产环境必须关掉。我用SLF4JLogback的三级控制按包分级!-- logback-spring.xml -- logger namecom.example.dao levelDEBUG additivityfalse appender-ref refFILE/ /logger logger namecom.example.service levelINFO/ root levelWARN appender-ref refCONSOLE/ /rootDAO层DEBUG日志写入文件Service层只INFO框架WARN以上才打屏。异步Appender防阻塞appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender appender-ref refFILE/ /appender避免高并发时日志IO拖慢HTTP响应。上线前强制检查在CI流水线加Shell脚本# 检查是否还有log.debug在生产代码中 grep -r log\.debug src/main/java/ --include*.java | grep -v test exit 1未通过则构建失败。最后说一句实在话所谓“JavaWeb开发实战源码”从来不是让你复制粘贴的代码包。它是你亲手把web.xml的每一行配对、把mybatis-config.xml的每一个property敲出来、把Tomcat日志里每一行SEVERE错误追到底的肌肉记忆。我带过的实习生最快两周能独立修复404最慢的卡在url-pattern匹配规则上一个月——差别不在智商而在是否愿意把官方文档里那句“the container maps the request to the servlet”真的拆开看容器怎么map怎么servlet。希望帮到你。本文还有配套的精品资源点击获取
返回列表