简介javaweb框架源码-baltika是一份基于Spring Framework编写的Java Web应用开源源码项目面向想通过读源码吃透Spring MVC请求处理流程、依赖注入、AOP与事务管理等核心机制的初中级Java开发人员也适合准备进行框架级源码分享的进阶学习者。压缩包共48个文件含10个html、10个js、7个css、6个java源码及pom.xml等xml配置、字体素材与README说明整体仅1.04MB前端页面与后端业务代码、配置文件分区存放目录结构紧凑清晰便于按模块对照学习。目前已有171人学习下载。项目采用MVC分层设计涵盖Controller、Service、DAO三层并涉及Spring Data JPA或MyBatis的数据访问整合、声明式事务、Spring Security安全配置、模板引擎渲染、异常处理与国际化等典型内容能帮读者把注解背后的对象装配逻辑和切面织入方式真正看清楚避开繁琐的框架配置弯路对想推进源码级研读、借鉴框架最佳实践的Java Web开发者是一份高性价比的学习样本。1. baltika是什么一个把Spring MVC调用链摊开给你看的Web工程假设你刚入职接手一套老代码领导丢给你一句“先跑起来看看”结果你盯着源码看了一上午连入口在哪都没找到。baltika这个Java Web工程就是为这种事准备的——它把Spring Framework的完整调用链压缩到一个结构清晰的工程里什么组件都在它该在的地方。这份源码实现的是一个典型MVC架构的Web应用DispatcherServlet怎么拦截请求、Controller怎么绑定参数、Service层事务怎么切、DAO层怎么查库、视图解析器怎么把逻辑名拼成JSP物理路径一条线理下来你对“一个请求从进入到返回”会有一种前所未有的通透感。它适合两类人一是学Spring MVC却拼不出完整Demo的新手需要一份能端到端跑起来的源码当参照二是准备接手改造老系统、想快速摸清项目结构的开发者。baltika用自己的代码把“系统开源”这件事落到了实处拉下来就能直接读。2. 架构与模块设计从web.xml到DispatcherServlet的任务接力2.1 总体架构一次请求的七步调用链baltika的运行模型可以概括成一场“请求接力赛”。我不止一次在调试这类老Spring工程时发现很多人把框架当黑盒用出了问题只能对着报错信息干瞪眼。baltika没有用Spring Boot那种自动化装配把细节藏起来而是老老实实按Spring Framework的经典姿势来组织。一个HTTP请求进来要经历七步Tomcat容器先解析请求头交给web.xml里注册的DispatcherServletDispatcherServlet通过HandlerMapping找到对应Controller的处理方法进入方法前Spring先完成参数绑定和校验Controller把业务逻辑委托给Service层事务边界在此打开Service调用DAO层做数据库读写结果被封装成ModelAndView最后视图解析器把逻辑视图名解析成JSP物理路径渲染成HTML返回浏览器。这一条链路就是Spring MVC的运转骨架。baltika的优点在于每一步你都能在源码里找到对应的配置声明没有隐藏魔法。我建议第一次读这份源码的人先不要钻进代码细节而是带着这条七步链路去翻工程每找到一环节就在配置文件里做个标记这样一遍走下来Spring MVC的骨架就刻在脑子里了。后续改任何功能你都能快速定位问题出在哪一层——是Web层没接到参数还是Service层事务没生效还是DAO层SQL写错。2.2 目录结构源码工程的完整地图baltika按Maven标准布局组织拿到手不需要额外适配就能导入IDE。下面是源码的目录树baltika/ ├── pom.xml ├── README.md └── src/ ├── main/ │ ├── java/ │ │ └── com/baltika/ │ │ ├── config/ │ │ │ ├── AppConfig.java │ │ │ ├── WebConfig.java │ │ │ └── DatabaseConfig.java │ │ ├── controller/ │ │ │ └── UserController.java │ │ ├── service/ │ │ │ ├── UserService.java │ │ │ └── impl/ │ │ │ └── UserServiceImpl.java │ │ ├── dao/ │ │ │ ├── UserDao.java │ │ │ └── impl/ │ │ │ └── UserDaoImpl.java │ │ └── model/ │ │ └── User.java │ ├── resources/ │ │ ├── application.properties │ │ └── log4j.properties │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── views/ │ │ ├── index.jsp │ │ ├── userList.jsp │ │ └── userForm.jsp │ └── static/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── common.js这个结构是典型的Spring MVC分层。config包放Java配置类替代老版本里的Spring XMLcontroller包是Web层入口接收请求并返回视图service包做业务逻辑讲究接口与实现分离dao包负责数据库访问同样接口与实现分开model包里是实体类。resources目录下application.properties管理数据源之类的外部化配置log4j.properties控制日志输出。webapp/WEB-INF/views下面是JSP视图webapp/static里放CSS和JS。我建议二次开发时沿用这个分工不要图省事把SQL直接写到Controller里。baltika这份工程规模不大但包结构的边界很清晰依赖方向从Controller指向ServiceService指向DAO每个包都只依赖下一层没有环形依赖。这种约束是老Spring工程能长期维护的前提改的时候也别破坏它。2.3 web.xmlServlet容器与Spring的接驳点先看web.xml它是整个Web应用的入口配置文件?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_3_1.xsd version3.1 display-namebaltika/display-name !-- 创建Spring根容器 -- context-param param-namecontextClass/param-name param-valueorg.springframework.web.context.support.AnnotationConfigWebApplicationContext/param-value /context-param !-- 指定根容器的配置类 -- context-param param-namecontextConfigLocation/param-name param-valuecom.baltika.config.AppConfig/param-value /context-param !-- 初始化根容器 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 注册DispatcherServlet -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextClass/param-name param-valueorg.springframework.web.context.support.AnnotationConfigWebApplicationContext/param-value /init-param init-param param-namecontextConfigLocation/param-name param-valuecom.baltika.config.WebConfig/param-value /init-param load-on-startup1/load-on-startup /servlet !-- 映射所有请求到DispatcherServlet -- servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app这段配置的核心是双容器模型。ContextLoaderListener创建的根容器加载AppConfig管理Service、DAO、DataSource这些后端组件DispatcherServlet自己管理的子容器加载WebConfig管理Controller、视图解析器、HandlerMapping。子容器可以访问父容器的Bean但父容器看不到子容器的Bean。这样分层的好处是Web层的组件即使被误改也不会直接影响核心业务Bean。有两个细节经常有人问。load-on-startup写成1是让Tomcat启动时立即初始化DispatcherServlet而不是等第一个请求进来才初始化方便在启动阶段就暴露配置错误。servlet-mapping的url-pattern写的是根路径“/”意味着baltika接管所有请求包括静态资源也都被拦了进来。因此必须在WebConfig里补静态资源映射否则页面会因为缺失CSS而裸奔。这个坑我在避坑章节还会专门讲。2.4 AppConfig与DatabaseConfig根容器里的业务BeanAppConfig是根容器的核心配置类package com.baltika.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.transaction.annotation.EnableTransactionManagement; Configuration ComponentScan(basePackages com.baltika.service) Import(DatabaseConfig.class) EnableTransactionManagement public class AppConfig { }ComponentScan只扫service包理由是根容器只管业务Bean。Import把DatabaseConfig拉进来让数据源和事务管理器也归根容器管理。EnableTransactionManagement开启注解事务这样Service类里标了Transactional的方法会自动织入事务管理。DatabaseConfig负责数据源和JdbcTemplatepackage com.baltika.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import javax.sql.DataSource; Configuration ComponentScan(com.baltika.dao) PropertySource(classpath:application.properties) public class DatabaseConfig { Value(${jdbc.driver}) private String driver; Value(${jdbc.url}) private String url; Value(${jdbc.username}) private String username; Value(${jdbc.password}) private String password; Bean public DataSource dataSource() { DriverManagerDataSource dataSource new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }这里用PropertySource加载application.properties里的配置再用Value注入。注意JdbcTemplate是构造器注入DataSource接口清晰且方便单元测试。如果你的数据库环境是连接池把DriverManagerDataSource换成DBCP或HikariCP也只需要改这一个Bean的回调别的代码不用动。application.properties的内容很简单jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/baltika?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456到这里根容器的Bean工厂基本装配完毕DataSource在DatabaseConfig里产生JdbcTemplate在DatabaseConfig里产生Service在AppConfig的扫描里注册DAO在DatabaseConfig的扫描里注册。剩下的Controller交给WebConfig的子容器去管。3. 核心代码拆解一个请求从进入到返回的完整链路3.1 WebConfigSpring MVC的装配车间WebConfig是DispatcherServlet子容器的配置类。前面说过url-pattern写的是“/”所有请求都会进DispatcherServlet所以WebConfig必须把Spring MVC的“三大件”配置好视图解析器、静态资源映射、注解驱动。package com.baltika.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; Configuration EnableWebMvc ComponentScan(com.baltika.controller) public class WebConfig implements WebMvcConfigurer { Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver new InternalResourceViewResolver(); resolver.setPrefix(/WEB-INF/views/); resolver.setSuffix(.jsp); return resolver; } Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(/static/); } }EnableWebMvc相当于XML命名空间里的mvc:annotation-driven它把Spring MVC需要的那一堆默认组件RequestMappingHandlerMapping、RequestMappingHandlerAdapter等全部注册进子容器。InternalResourceViewResolver是视图解析器的标准实现setPrefix指定JSP文件放在/WEB-INF/views/目录下setSuffix指定文件后缀是.jsp。这样Controller里返回字符串userList时视图解析器会拼出/WEB-INF/views/userList.jsp这个物理路径。基础路径放在WEB-INF下能避免用户直接通过URL访问JSP页面多一层安全保护。3.2 UserController请求的接收与转发UserController是Web层的核心入口baltika的用户管理模块就在这里处理请求package com.baltika.controller; import com.baltika.model.User; import com.baltika.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; Controller public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping(/) public String index() { return index; } GetMapping(/user/list) public String list(Model model) { ListUser users userService.findAll(); model.addAttribute(users, users); return userList; } GetMapping(/user/add) public String showAddForm() { return userForm; } PostMapping(/user/add) public String add(RequestParam(username) String username, RequestParam(age) Integer age) { userService.addUser(username, age); return redirect:/user/list; } }这个Controller有四个映射路径。GetMapping(/)直接返回字符串index交给视图解析器解析成index.jspGetMapping(/user/list)从Service取用户列表放进Model视图名userList渲染出表格GetMapping(/user/add)只是返回新增表单页PostMapping(/user/add)接收表单提交的两个参数调用Service写入数据库后重定向到列表页。这里用构造器注入UserService是Spring官方推荐的姿势配合Service注解和Spring 4.3的隐式注入可以不写Autowired字段注入。注意add方法的返回值是redirect:/user/list这是POST提交后防止刷新重复提交的标准做法。浏览器收到这个响应后会重新GET一次/user/list再次刷新页面也不会触发表单重复提交。3.3 UserService接口与实现分离的业务层Service接口定义了两个业务动作package com.baltika.service; import com.baltika.model.User; import java.util.List; public interface UserService { ListUser findAll(); void addUser(String username, Integer age); }实现类加上事务注解package com.baltika.service.impl; import com.baltika.dao.UserDao; import com.baltika.model.User; import com.baltika.service.UserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service Transactional public class UserServiceImpl implements UserService { private final UserDao userDao; public UserServiceImpl(UserDao userDao) { this.userDao userDao; } Override Transactional(readOnly true) public ListUser findAll() { return userDao.findAll(); } Override public void addUser(String username, Integer age) { userDao.insert(username, age); } }类上标了Transactional意味着所有public方法默认开启事务。findAll方法单独标了readOnly true提示数据库驱动可以使用只读模式优化这是写查询方法时值得养成的习惯。addUser没有显式标注沿用类上的事务配置任何RuntimeException抛出都会触发回滚。如果你在这个工程里新增一个批量导入逻辑要特别小心不要在方法内部try-catch吞掉异常后再调用多个DAO方法那样事务会提前提交造成数据只写了一半的翻车现场。3.4 UserDao用JdbcTemplate操作数据库DAO接口定义数据访问契约package com.baltika.dao; import com.baltika.model.User; import java.util.List; public interface UserDao { ListUser findAll(); void insert(String username, Integer age); }实现类用JdbcTemplate干活package com.baltika.dao.impl; import com.baltika.dao.UserDao; import com.baltika.model.User; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import javax.sql.DataSource; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; Repository public class UserDaoImpl implements UserDao { private final JdbcTemplate jdbcTemplate; public UserDaoImpl(DataSource dataSource) { this.jdbcTemplate new JdbcTemplate(dataSource); } Override public ListUser findAll() { String sql SELECT id, username, age FROM t_user ORDER BY id DESC; return jdbcTemplate.query(sql, new UserRowMapper()); } Override public void insert(String username, Integer age) { String sql INSERT INTO t_user(username, age) VALUES(?, ?); jdbcTemplate.update(sql, username, age); } class UserRowMapper implements RowMapperUser { Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setAge(rs.getInt(age)); return user; } } }JdbcTemplate通过构造器拿到DataSource。注意update方法的两个参数username和age是可变参数预处理语句里的问号占位符按顺序绑定JdbcTemplate内部会完成参数类型转换和资源释放。query方法接收一个RowMapper回调把结果集每一行映射成一个User对象。用JdbcTemplate的好处是它把连接获取、预编译、结果集遍历、异常转换这些样板代码全封装掉了你只需要写SQL和行映射逻辑。这套代码虽然直接但SQL字符串硬编码在DAO里后续如果SQL变复杂可以考虑换成MyBatis或Spring Data JPA不过baltika目前的规模完全够用。4. 运行指南从Clone到页面渲染的完整落地步骤4.1 环境准备JDK、Maven、Tomcat的版本搭配我在本地跑通baltika用的环境是JDK 1.8、Maven 3.6.3、Tomcat 8.5.24、MySQL 5.7.30。选这套组合没有玄学讲究纯粹因为它和Spring 5.1.x的兼容性经过最充分验证。JDK 8和Tomcat 8.5都在Servlet 3.1规范上对齐不会出现Class文件版本解析失败的问题。如果你机器上只有JDK 11Tomcat换9.x就能跑但建议把pom.xml里maven-compiler-plugin的source和target保持1.8避免编译级别过高带来不必要的坑。我不建议在这种教学型老工程上挑战JDK 17Spring 5.1在老JDK上运行更稳没必要给自己找不痛快。MySQL驱动单独提醒一句。MySQL 5.7用com.mysql.jdbc.DriverMySQL 8.x必须用com.mysql.cj.jdbc.Driver且8.x驱动强制要求连接URL带serverTimezoneAsia/Shanghai。很多人启动时直接ClassNotFoundException就是栽在这。工程自带pom如果依赖的驱动是5.1.x而你本机是MySQL 8要同步改三处pom.xml里的驱动坐标、DatabaseConfig里的jdbc.driver字符串、application.properties里jdbc.url的时区参数缺一个都不行。4.2 第一步建库建表准备数据baltika用户管理功能依赖一张t_user表。先建库再建表SQL如下CREATE DATABASE IF NOT EXISTS baltika DEFAULT CHARACTER SET utf8mb4; USE baltika; CREATE TABLE IF NOT EXISTS t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, age INT DEFAULT 0 ); INSERT INTO t_user(username, age) VALUES (zhangsan, 25), (lisi, 30);执行顺序很重要先建库再USE进入库然后建表。字符集必须用utf8mb4否则JSP表单提交中文时会出现乱码。如果你用的是MySQL 8只要驱动和连接参数按4.1处理好这段SQL不会有兼容问题。表里的两条测试数据是为了验证列表页效果也可以不插入但建议至少留一条能确认查询链路通了。4.3 第二步修改数据源配置打开src/main/resources/application.properties把jdbc.password和jdbc.username改成你本机MySQL能用的值。jdbc.url里的localhost建议改用127.0.0.1能绕开一部分MySQL对本机socket的认证差异。改完别着急下一步先确认MySQL服务确实在3306端口监听。我在调试时遇到过好多次连接被拒最后发现是MySQL根本没启动白蹲了半小时。4.4 第三步Maven构建与部署在工程根目录执行mvn clean package -DskipTestsclean会清掉target目录package把项目打成war包。-DskipTests跳过单元测试因为baltika没配测试用例不跳也只会跑零个测试。构建成功的标志是target目录下生成baltika.war。然后把war包拷到Tomcat的webapps目录下启动Tomcatbin目录下的startup.sh是Linux/macOS启动脚本startup.bat是Windows的。Tomcat启动后会自动解压baltika.war。这段pom.xml的关键配置决定了构建产物properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependencies build finalNamebaltika/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.2.3/version /plugin /plugins /buildfinalName决定war包名也就是应用上下文路径。如果把它改成myweb访问地址就变成http://localhost:8080/myweb/。如果你在IDE里调试也可以不用war部署在IDEA中设置Tomcat Server把baltika作为Artifact添加进去通过Run模式启动好处是能直接在Controller方法里打断点看参数值。4.5 第四步验证请求链路启动完成后依次访问三个地址验证首页、列表页、新增页。http://localhost:8080/baltika/ 看欢迎页能否渲染http://localhost:8080/baltika/user/list 看列表页能否查到数据库记录http://localhost:8080/baltika/user/add 看表单页能否打开如果列表页500去Tomcat的logs/localhost.log里找异常栈如果是SQLGrammarException先看表名和SQL是不是t_user。如果页面能打开但没样式静态资源映射的问题出在WebConfig如果中文乱码优先查JSP的pageEncoding和数据库URL编码参数。这几个问题在下一章的避坑列表里有更完整的展开。这套验证流程走一遍能确认三层链路都通DispatcherServlet接请求首页通、Controller转业务列表页通、DAO读写库数据渲染通。如果你只是想研究源码而不打算部署也可以依赖这套验证逻辑在IDE里单跑但baltika没集成测试框架我建议还是老老实实部署一次效率其实更高。5. 避坑指南跑baltika时最常踩的5个坑5.1 访问页面全404Controller映射明明没问题现象Tomcat启动正常日志也没有报错但访问/user/list返回404。检查Controller注解和映射路径看起来都对。原因最常见的是DispatcherServlet的servlet-mapping写错了。有人把它写成/*于是DispatcherServlet把JSP文件本身的请求也当成Controller路径去匹配结果所有视图都解析不了。另一个可能是UserController类漏了Controller注解或者WebConfig的ComponentScan没覆盖controller包。解决先看web.xml里servlet-mapping的url-pattern根路径应写作/而不是/*。再确认controller包在ComponentScan的basePackages范围内。改完做一次mvn clean package重新部署因为Tomcat解压目录可能有旧class缓存。5.2 页面渲染出来但CSS和JS全部404现象HTML内容正常返回但页面样式全丢打开Network面板看到static/css/style.css返回404。原因DispatcherServlet用/拦截了所有请求而WebConfig里没有配置静态资源映射或者JSP里引用的路径和映射对不上。很多从Spring Boot转回传统Spring工程的人会漏这一步因为Spring Boot默认把classpath:/static/当静态资源目录而传统Spring MVC完全靠手工配置。解决在WebConfig里重写addResourceHandlers方法注册/static/**映射到/static/目录同时检查JSP里的引用路径建议统一写成${pageContext.request.contextPath}/static/css/style.css这样不管部署在什么上下文路径下都不会错。url-pattern写成/的同时必须配addResourceHandlers这是传统Spring MVC的铁律。5.3 数据库连接失败Access denied现象启动Tomcat后访问列表页页面报500日志里有Access denied for user rootlocalhost。原因application.properties里的jdbc.username或jdbc.password和本机MySQL不一致。还有一种情况是MySQL 8的caching_sha2_password认证插件和旧版驱动不兼容也会报Access denied。解决逐项核对properties里的driver、url、username、password。MySQL 5.7用com.mysql.jdbc.DriverMySQL 8改成com.mysql.cj.jdbc.Driver并加上serverTimezoneAsia/Shanghai。如果确认密码没错再把localhost改成127.0.0.1试一次能绕开一部分socket认证差异。5.4 Service注入DAO时报No qualifying bean of type现象Tomcat启动时或首次请求时报NoSuchBeanDefinitionException提示找不到com.baltika.dao.UserDao类型的Bean。原因UserDaoImpl类上漏了Repository注解或者DatabaseConfig的ComponentScan没有覆盖dao包。Spring容器里根本没有UserDao类型的BeanService注入自然失败。解决给UserDaoImpl加上Repository检查DatabaseConfig的ComponentScan路径是否包含com.baltika.dao如果用了构造器注入再确认构造函数是public。顺手说一句不建议用Autowired字段注入在Service和DAO数量多的时候容易出循环依赖构造器注入至少能让IDE帮你检查依赖完整性。5.5 改了JSP却迟迟不生效现象修改userList.jsp后刷新页面内容还是旧的。原因Tomcat默认对webapps下已解压目录的JSP做了缓存也可能是IDE没有触发重新部署。baltika是war包部署模式不是Spring Boot的jar内嵌容器没有devtools热更新这样的后悔药。解决先执行mvn clean package重建war删掉Tomcat webapps下旧的baltika目录再重新放war包。用IDE调试的话手动触发Tomcat的Redeploy或者检查Artifacts是否勾选了Build on frame deactivation。注意想彻底避免这类缓存问题开发阶段可以把Tomcat的conf/web.xml里reloadable设为true虽然牺牲一点性能但至少改配置不用反复重启整个容器。6. 二次开发给baltika加一个删除用户的功能拿到baltika源码后第一个练手改造我建议做一个“删除用户”功能。它能把Controller、Service、DAO三层都穿透一遍改动量又小不会让你刚上手就淹没在细节里。改造路径很清晰先改DAO接口和实现再改Service接口和实现然后在Controller里加一个删除映射最后在userList.jsp上补一个删除按钮。DAO层加一个方法// UserDao接口 void deleteById(Long id); // UserDaoImpl实现 Override public void deleteById(Long id) { String sql DELETE FROM t_user WHERE id ?; jdbcTemplate.update(sql, id); }Service层透传Controller加POST映射PostMapping(/user/delete) public String delete(RequestParam(id) Long id) { userService.deleteById(id); return redirect:/user/list; }JSP里用内联表单提交不使用a标签的GET链接c:forEach items${users} varuser tr td${user.id}/td td${user.username}/td td${user.age}/td td form methodpost action${pageContext.request.contextPath}/user/delete styledisplay:inline; input typehidden nameid value${user.id}/ button typesubmit删除/button /form /td /tr /c:forEach注意JSP头部需要引入JSTL标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。这里有一个值得留意的设计细节删除操作我坚持用POST而不是GET。删除是修改操作放GET路径会让爬虫或浏览器预加载误触发也有CSRF风险。加完这几处后验证重新构建部署列表页选中一条点删除返回后数据消失再直接访问/user/delete?id1这类GET地址应返回405或跳转证明POST约束生效。这个验证方式我每次改造都会过一遍特别是涉及写操作时一定能确保请求边界是收紧的。我一直以来的改造习惯是改一个需求先把调用链上每一层要动的文件列出来再动手。如果你在改baltika时遇到某个Bean突然注入失败优先检查我列过的那五个坑——扫描路径、注解遗漏、事务回滚被吞、静态资源映射、Tomcat缓存。这些是我在多个老Spring工程上反复交过的学费每一条都能让你白蹲一下午。希望帮到你。本文还有配套的精品资源点击获取