☰
JavaWeb实战源码:从Servlet生命周期到Spring事务避坑指南
2026/9/30 1:04:36 网站建设 项目流程

简介:这是一份面向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层JSP+CSS+JS协同渲染、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开发实战源码》不是教学视频的配套附件,而是一套按企业级项目节奏组织的、带完整调试断点和日志埋点的可运行工程。它覆盖从传统Servlet+JSP+MySQL三层架构,到Spring MVC+MyBatis+Druid+Logback的轻量整合,再到Spring Boot 2.7.x + Thymeleaf + HikariCP的现代演进路径。适合两类人:刚学完Servlet API但写不出完整登录注册流程的在校生;以及能写Spring Boot但一碰Filter链或自定义ViewResolver就发懵的转岗开发者。它不教“怎么配Tomcat”,而是让你亲手把web.xml里那行<filter-mapping>拖进Debug模式,单步看到它如何被ApplicationFilterChain组装、执行、传递request——这才是“实战”的本义:代码可停、可查、可改、可证伪。


2. 用标准Maven结构跑通第一个Servlet+JSP项目:从零建工程、配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 --> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <servlet.version>4.0.1</servlet.version> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version> <scope>provided</scope> </dependency> </dependencies>

提示:<scope>provided</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.jsp

src/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 version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.web.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> <!-- 值越小优先级越高 --> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

<load-on-startup>1</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 验证请求流转:用浏览器+Console+Debug三线并行抓包

启动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是完整路径(含ContextPath),servletPath是匹配到的Servlet路径部分。若部署路径为/myapp,访问/myapp/hello,则requestURI=/myapp/hello,servletPath=/hello。这是做RESTful路由时判断路径层级的关键依据。


3. 从JSP到Thymeleaf:为什么模板引擎切换不是改后缀那么简单?

3.1 JSP的隐式对象与EL表达式失效的三大场景

JSP里写${user.name}很自然,但新手常遇到“明明request.setAttribute("user", u)了,JSP里却显示空白”。这不是EL语法错,而是三个隐藏开关没打开:

  1. page指令未启用EL:JSP默认开启EL,但若web.xml声明的是Servlet 2.3或更低版本,EL会被禁用。检查web.xml的version="4.0"是否生效;
  2. isELIgnored="true"显式关闭:在JSP顶部加<%@ page isELIgnored="true" %>会全局禁用EL,删掉即可;
  3. 对象属性无getter方法:User类必须有getName(),不能只有name字段。JSP通过JavaBean规范反射调用getter,字段直访不支持。

更隐蔽的问题是JSTL标签库未正确引入。想用<c:forEach>遍历List,必须在JSP顶部声明:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

且pom.xml中添加依赖:

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.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 action="login" method="post"> <input type="text" name="username" value="<%= request.getParameter("username") != null ? request.getParameter("username") : "" %>"> <input type="password" name="password"> <input type="submit" value="Login"> </form>

Thymeleaf等效写法:

<form th:action="@{/login}" th:method="post"> <input type="text" name="username" th:value="${param.username ?: ''}"> <input type="password" name="password"> <input type="submit" value="Login"> </form>

关键变化:

  • th:action="@{/login}":@{}是URL重写语法,自动添加ContextPath,避免硬编码/myapp/login;
  • th:value="${param.username ?: ''}":param是Thymeleaf内置对象,等价于request.getParameter();?:是Elvis操作符,防NPE;
  • 没有<% %>脚本块:Thymeleaf禁止Java代码嵌入,强制逻辑与视图分离。

3.3 在Spring Boot中集成Thymeleaf:自动配置背后的三个Bean

Spring Boot的spring-boot-starter-thymeleaf看似一键集成,实则暗藏三个关键Bean的自动装配:

Bean名称类型作用可调参数
TemplateResolverITemplateResolver解析模板位置、编码、缓存策略spring.thymeleaf.cache=false(开发关缓存)
TemplateEngineTemplateEngine执行模板渲染的核心引擎spring.thymeleaf.enabled=true(默认true)
ThymeleafViewResolverViewResolver将逻辑视图名(如"login")映射为物理路径(如/templates/login.html)spring.thymeleaf.prefix=classpath:/templates/

验证是否生效:在application.properties中加一行:

logging.level.org.thymeleaf=DEBUG

启动时若看到[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.prefix=file: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 version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <typeAliases> <package name="com.example.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <!-- 使用JDBC事务,非Spring管理 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/javaweb?useSSL=false&amp;serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/example/mapper/UserMapper.xml"/> </mappers> </configuration>

重点解析:

  • <typeAliases>:为com.example.entity.User注册别名User,使Mapper XML中可直接写resultType="User",无需全限定名;
  • <transactionManager type="JDBC"/>:表示事务由JDBC Connection控制,conn.commit()/conn.rollback()生效。若后续接入Spring,则改为type="MANAGED",交由Spring的DataSourceTransactionManager接管;
  • <dataSource type="POOLED">:使用MyBatis内置连接池(非HikariCP),适合学习;生产环境必须换<dataSource type="UNPOOLED">配合Druid/HikariCP;
  • <mapper 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 id="selectByTable" resultType="User"> SELECT * FROM ${tableName} WHERE id = #{id} </select>

这里${tableName}是必须的,因为表名不能参数化。但若tableName来自用户输入,就构成严重漏洞。正确做法是白名单校验:

public List<User> selectByTable(String tableName, Long id) { // 白名单校验表名 List<String> 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 id="selectAll" resultType="User"> 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%的失效源于以下三点:

  1. 自调用失效:Service A的methodA()调用本类的methodB(),即使methodB()加了@Transactional,事务也不生效。因为Spring事务基于代理(Proxy),自调用绕过代理,直接走this引用。

    @Service public class UserService { public void methodA() { methodB(); // ❌ 绕过代理,事务不生效 } @Transactional public void methodB() { ... } // ✅ 但这里不会被代理拦截 }
  2. 异常类型不对:@Transactional默认只对RuntimeException及其子类回滚。若方法抛出IOException,事务不会回滚。必须显式声明:

    @Transactional(rollbackFor = Exception.class) public void updateUser() throws IOException { ... }
  3. 传播行为误解:@Transactional(propagation = Propagation.REQUIRED)(默认)表示“有事务则加入,无则新建”。但若Service A调用Service B,B的事务属性是REQUIRES_NEW,则A的事务会被挂起,B执行完再恢复A——这可能导致A的DB操作在B的事务提交后才写入,违反一致性。

排查技巧:在application.properties中开启事务日志:

logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

启动后若看到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配置界面看似简单,但以下五处填错会导致项目启动即失败:

  1. Application context(上下文路径):
    默认是/,但若你项目名叫javaweb-demo,实际访问路径是http://localhost:8080/javaweb-demo/。若在此处填/,则IDEA会把项目部署到ROOT,覆盖默认欢迎页;若填/myapp,则必须访问http://localhost:8080/myapp/。建议留空,让IDEA自动取模块名。

  2. Deployment的Artifact选择:
    必须选中javaweb-demo:war exploded(带exploded),而非javaweb-demo:war。前者是解压部署,修改JSP/HTML可热更新;后者是打包部署,每次改完都要重新Build Artifact。

  3. Server Options的On ‘Update’ action:
    设为Update classes and resources,这样Ctrl+F10热更新时,Java类和静态资源都会同步。若选Redeploy,会重启整个Context,Session丢失。

  4. Startup/Connection的JDK配置:
    Tomcat启动JVM的JDK必须与项目Module SDK一致。若项目用JDK 1.8,而Tomcat配置里选了JDK 11,会出现Unsupported major.minor version 52.0(JDK 1.8字节码版本是52)。

  5. 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/当前Host(localhost)下所有WebApp的ServletContext.log()、ServletConfig.getServletContext().log()输出Servlet初始化异常、Filter链中断
localhost_access_log.<date>.txt$CATALINA_HOME/logs/HTTP访问日志,格式类似Apache:127.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 isELIgnored="true" %>,删掉;
  • 第三层:确认User类有getName()方法,且name字段是private(JavaBean规范要求getter/setter)。
现象:表单提交后中文乱码(如“张三”变“å¼ ä¸‰”)
  • 第一层:在Servlet中System.out.println(new String(req.getParameter("username").getBytes("ISO-8859-1"), "UTF-8")),若能正确打印,说明是编码问题;
  • 第二层:在web.xml中加Filter:
<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
  • 第三层:确认HTML表单指定了accept-charset="UTF-8":
<form accept-charset="UTF-8" method="post" action="/login">

避坑总结:

  1. 404不是路径错,是映射没生效:90%的404源于web.xml未被Tomcat读取(检查web.xml是否在WEB-INF/下,且web.xml开头的xsi:schemaLocationURL能否访问);
  2. 500不是代码错,是类加载失败:java.lang.ClassNotFoundException最常见,检查pom.xml依赖是否<scope>provided</scope>误用,或WEB-INF/lib/下jar包是否缺失;
  3. 400不是前端错,是参数解析失败:HTTP Status 400 – Bad Request多因@RequestParam要求必填但前端未传,或日期格式不匹配(如@DateTimeFormat(pattern="yyyy-MM-dd")但传了2024/01/10)。

6. 从源码到生产:三个让JavaWeb项目真正落地的关键习惯

6.1 把web.xml当作API契约文档来维护

很多团队把web.xml当成一次性配置文件,改完就扔。但它是整个Web应用的入口契约,必须像接口文档一样管理。我坚持三个动作:

  1. 用XML注释标注每个配置的业务含义:

    <!-- 【登录认证Filter】拦截所有 /admin/** 请求,校验Session中user是否为空 --> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>

    这比写Wiki文档更及时,因为代码和注释永远在一起。

  2. 用<display-name>统一命名应用:

    <display-name>JavaWeb-User-Management-v1.2</display-name>

    在Tomcat Manager页面一眼识别版本,避免测试环境部署错包。

  3. 把<error-page>当监控探针:

    <error-page> <error-code>500</error-code> <location>/error/500.jsp</location> </error-page> <error-page> <exception-type>java.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:

  1. 在pom.xml加依赖:
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency>
  1. 创建src/main/resources/db/changelog/db.changelog-master.yaml:
databaseChangeLog: - include: file: db/changelog/v1.0/create_user_table.yaml - include: file: db/changelog/v1.1/add_index_to_username.yaml
  1. 每次建表/加字段,都写一个独立的vX.X/change.yaml,Liquibase自动记录DATABASECHANGELOG表,保证每次mvn compile都精准执行未执行的变更。

好处:新同事拉代码后,mvn clean compile自动建库建表,不用问“init.sql在哪”“要先跑哪些脚本”。

6.3 日志分级与异步化:让debug日志不拖垮生产环境

本地开发时log.debug("SQL: {}", sql)很爽,但生产环境必须关掉。我用SLF4J+Logback的三级控制:

  1. 按包分级:

    <!-- logback-spring.xml --> <logger name="com.example.dao" level="DEBUG" additivity="false"> <appender-ref ref="FILE"/> </logger> <logger name="com.example.service" level="INFO"/> <root level="WARN"> <appender-ref ref="CONSOLE"/> </root>

    DAO层DEBUG日志写入文件,Service层只INFO,框架WARN以上才打屏。

  2. 异步Appender防阻塞:

    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE"/> </appender>

    避免高并发时日志IO拖慢HTTP响应。

  3. 上线前强制检查:
    在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。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询