简介:Baltika是一套基于Spring Framework的Java Web开源项目源码,适合正在学习Java Web开发、希望通过实际代码理解Spring MVC、依赖注入、AOP以及数据访问层实现的开发者。压缩包共48个文件、大小约1.04MB,内容涵盖Java类、HTML页面、JavaScript、CSS样式、XML配置、字体文件和README文档,既有后端逻辑也有前端展示资源,结构相对完整。目前已有171人学习/下载。通过研读这套源码,可以清晰看到Controller、Service、DAO的分层协作方式,了解Spring容器如何管理Bean、处理请求与视图渲染,还能参考其异常处理、配置管理、国际化等常见写法,从而快速建立Spring Web项目的整体开发思路,尤其适合作为入门后的实践读本。
1. 初看 baltika:这不是 Demo,而是一套带完整分层的 Spring Web 系统
某开发者从仓库拉下 baltika 源码包,第一反应是“这比课程设计里那些三五个 Java 文件的工程靠谱多了”。它是一套基于 Spring Framework 的 Java Web 应用程序源码,系统开源得相当完整:Controller、Service、持久层、前端页面、SQL 初始化脚本全都齐活。它能解决一个很实际的诉求——你想要一个能跑、能改、能拿去二次开发的 Web 系统底座,而不是再从零手搓登录和数据管理。适合刚学完 Spring 想进阶的开发者,也适合准备毕业设计或公司内部小系统的同学;熟手则可以从它的分层和配置里快速找到一套可复用的工程模板。
2. 技术栈与工程结构:从 pom.xml 到 webapp 的完整依赖地图
要判断一套 Java Web 源码值不值得读,我一般先不看业务代码,而是先打开两份文件:pom.xml 和 web.xml。它们决定了这个工程用到了哪些组件、请求从哪个入口进来。baltika 的路子很直接,没有引入微服务那套复杂组件,就是一个经典的 Spring MVC + MyBatis + Maven 的单体 Web 应用。这种组合放在今天看不算新,但正因为不新,它踩坑少、资料多、跑起来稳,特别适合拿来当底座改造。
2.1 技术栈盘点:为什么 Spring Framework 单品依然适合做系统底座
先看 Maven 依赖里都装了什么。核心无非这几样:Spring Framework(包含 spring-webmvc、spring-jdbc、spring-tx)、MyBatis 以及 mybatis-spring 桥接包、MySQL 驱动、Jackson 用于 JSON 转换,再加上 Servlet API。没有 Spring Boot,没有微服务网关,整就是一个传统的 war 包工程。这套选型的好处有两点:第一,Spring Framework 的 IoC 容器把对象生命周期管起来了,你写业务只用关心接口和实现,不用自己 new 依赖;第二,MyBatis 把 SQL 写在 XML 里,复杂查询调优时直接拿 SQL 去数据库执行一遍就能定位问题,不像 ORM 那样在代码和 SQL 之间来回翻译。
常见的疑问是“现在都 Spring Boot 了,还有必要看这种老式 war 工程吗”。我的看法是,Spring Boot 只是把 Spring Framework 的配置方式变了,底层容器、Bean 的加载机制、MVC 的分发流程没有任何变化。baltika 这类源码把 web.xml、spring-mvc.xml、spring-context.xml 摊开在你眼前,你反而能比用 Spring Boot 时更清楚地看到框架的组装过程。学完它再回看 Boot 的自动配置,会有一种“原来它背地里做的是这些事”的通透感。
2.2 目录结构:从 src/main/java 到 webapp 的资源全貌
解压源码后先扫目录,maven 标准布局,不用费劲找文件。完整的结构大致是这样的:
baltika/ ├── pom.xml ├── baltika.sql └── src/main/ ├── java/com/baltika/ │ ├── controller/ │ ├── service/ │ │ ├── impl/ │ │ └── *.java │ ├── mapper/ │ └── entity/ ├── resources/ │ ├── spring/ │ │ ├── spring-context.xml │ │ └── spring-mvc.xml │ ├── mapper/ │ │ └── *.xml │ ├── jdbc.properties │ └── log4j.properties └── webapp/ ├── WEB-INF/ │ ├── views/ │ └── web.xml └── static/ ├── css/ ├── js/ └── images/entity 放数据库表对应的实体,mapper 放数据访问接口,service 放业务逻辑,controller 放请求入口。resources 下的 spring 目录集中管理所有 Spring 配置,jdbc.properties 把数据库连接参数单独抽出来。webapp 下 WEB-INF/views 存 JSP 页面,static 存静态资源,互不干扰。这套分层的边界非常清晰:你想改页面就去 views,想改接口逻辑就去 service,想查 SQL 就去 resources/mapper,基本不会找错地方。
2.3 三份核心配置文件:web.xml、spring-context.xml 与 spring-mvc.xml 各自管什么
Spring 工程最容易让新手迷糊的就是配置文件太多,到底谁管谁。我拆这套源码时习惯按“启动顺序”来理解:Tomcat 启动先读 web.xml,它负责创建 Spring 的根容器;根容器去加载 spring-context.xml,把 service、mapper 这些业务 Bean 扫进来;DispatcherServlet 作为 Web 层的入口,单独加载 spring-mvc.xml,在里面扫描 controller、配置视图解析器和静态资源映射。
web.xml 是 Servlet 容器和 Spring 的桥。常见做法是配一个 ContextLoaderListener 来加载根上下文,再配 DispatcherServlet 加载 MVC 上下文,两套上下文共存的模式。spring-context.xml 里通常写组件扫描路径,排除掉 controller;spring-mvc.xml 则只扫描 controller。这样分层有个好处:业务 Bean 不依赖 Web 层,以后你想把 service 抽出去做单元测试,不需要启动 Web 容器。
3. 核心链路拆解:一个请求从进入到页面渲染的完整路径
这一章是全文最关键的部分。我们跟着一个请求走完整条链路:用户在浏览器输入地址,Tomcat 把请求交给 Spring,Spring 找到对应的 Controller 方法,调用 Service,Service 再查 Mapper,最终把数据填进页面返回给浏览器。整个过程中每一步都有对应的代码和配置,理解这条链路,你就拥有了修改任何 Spring MVC 工程的能力。
3.1 请求入口:web.xml 中的 DispatcherServlet 配置
先看 web.xml 里最核心的一段:
<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_3_1.xsd" version="3.1"> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-context.xml</param-value> </context-param> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>配置里的关键是两个 contextConfigLocation。ContextLoaderListener 加载的是整个应用的根上下文,里面放了 service、mapper、数据源;DispatcherServlet 加载的是 Web 层上下文,只包含 controller、视图解析器、拦截器这些和请求处理相关的 Bean。load-on-startup 设为 1,表示 Tomcat 启动时立即初始化 DispatcherServlet,而不是等第一个请求进来才创建。
url-pattern 写成/而不是*.do,意味着所有请求都进入 Spring MVC 统一分发。这也是我比较推荐的方式:一套工程里代码风格统一,不会出现有的接口走 Spring、有的直接打 Servlet 的情况。代价是你必须额外配置静态资源映射,否则 css、js 也会被 DispatcherServlet 拦截,页面会变得光秃秃的。这个坑后面避坑章节还会专门讲。
3.2 控制器层:@Controller 的映射与参数绑定
请求进入 DispatcherServlet 后,HandlerMapping 会根据 URL 找到对应的 Controller 方法。baltika 里的 Controller 写法是标准的注解风格:
@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, Model model) { PageResult<User> page = userService.findPage(pageNum, 10); model.addAttribute("page", page); return "user/list"; } }这段代码的触发路径是/user/list,方法名不重要,重要的是注解上的映射地址。@RequestParam(defaultValue = "1")表示 pageNum 参数可以缺省,不传时默认取 1,这是给前端省事的常见写法。Model 是 Spring 提供的数据容器,往里面塞page对象后,JSP 页面可以直接用 EL 表达式读取。
返回值"user/list"不是跳转地址的完整路径,而是一个逻辑视图名,具体怎么解析到 JSP 文件,要看 spring-mvc.xml 里配置的视图解析器。这个设计把 Controller 和物理文件解耦了,你改了 JSP 存放目录也不用动代码。Controller 里没有 setter、没有业务判断,只负责参数接收、调用 Service、把结果放进 Model,业务逻辑全部下沉到 Service 层。
3.3 业务层与持久层:Service 接口与 MyBatis Mapper 的协作
Controller 只依赖接口,不直接 new 实现类。Service 层在工程里一般拆成接口和 impl 两部分,接口定义方法,impl 写实现。这样做不是为了凑层数,而是为了后续测试和替换实现方便,比如以后接入 RPC 或者加缓存时,你只需要新增一个实现类,Controller 一行不用改。
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public PageResult<User> findPage(int pageNum, int pageSize) { int offset = (pageNum - 1) * pageSize; List<User> list = userMapper.selectPage(offset, pageSize); int total = userMapper.countAll(); return new PageResult<>(list, total); } }分页逻辑放在 Service 里处理,offset 的计算方式是典型的数据库分页,第 1 页偏移 0,第 2 页偏移 pageSize。传给 Mapper 的是一个偏移量和一个条数,SQL 里直接用 limit 实现。总条数单独查一次 count,是为了前端分页组件能算出总页数。这种写法比直接在 SQL 里算好调试,也符合多数后端工程的习惯。
对应的 Mapper 接口和 XML 长这样:
public interface UserMapper { List<User> selectPage(@Param("offset") int offset, @Param("pageSize") int pageSize); int countAll(); }<mapper namespace="com.baltika.mapper.UserMapper"> <select id="selectPage" resultType="com.baltika.entity.User"> SELECT id, username, real_name, status FROM sys_user ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select> <select id="countAll" resultType="int"> SELECT COUNT(*) FROM sys_user </select> </mapper>MyBatis 的 SQL 写在 XML 里,mapper 的 namespace 必须和接口全限定名一致,select 的 id 必须和接口方法名一致,这是 MyBatis 把 XML 和接口绑定起来的约定。#{}是预编译参数占位符,MyBatis 会把它转成 JDBC 的?,可以有效避免 SQL 注入。${}则是字符串拼接,一般只在动态排序字段时用,能不用就不用。resultType 直接写实体类全限定名,字段名和数据库列名一致时能自动映射;不一致时需要在 SQL 里起别名,或者配置驼峰映射。
3.4 视图渲染与静态资源:spring-mvc.xml 里的关键配置
Controller 返回"user/list"之后,InternalResourceViewResolver 会把这个逻辑视图名拼接成实际的 JSP 路径。spring-mvc.xml 里的配置是:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:annotation-driven/>prefix 加 suffix 拼出来的完整路径就是/WEB-INF/views/user/list.jsp。把 JSP 放在 WEB-INF 目录下是一种安全习惯,浏览器无法直接通过 URL 访问该目录下的文件,只能由服务端转发渲染。mvc:resources这行是静态资源映射,把/static/**的请求交给默认 Servlet 处理,不走 Controller。如果少了这行,你访问 css 时 DispatcherServlet 会尝试去找一个叫 css 的 Controller 方法,结果必然是 404。mvc:annotation-driven是 3.0 以后必须加的,它负责注册请求映射、参数解析、JSON 转换这些基础组件。
4. 本地跑通:从环境准备到 Tomcat 部署的落地步骤
看完了代码,下一步就是把工程跑起来。这一章不会让你去改任何一个 Java 文件,只做环境和部署层面的操作。照着做,十分钟内应该能看到登录页面。如果你用的工具版本不太一样,我会在每步后面说明常见问题和替代方案。
4.1 环境准备:JDK、Maven、MySQL 与 Tomcat
先确认基础环境。JDK 建议 8 或 11,Maven 3.6 以上,MySQL 5.7 或 8.0,Tomcat 8.5 或 9.0。这套工程是传统的 war 包部署方式,不需要安装 Spring Boot 那套内嵌容器,直接装一个独立 Tomcat 就行。版本搭配注意一点:Tomcat 10 之后就把 javax.* 换成了 jakarta.*,老工程里的 Servlet 依赖跑不起来,所以优先用 Tomcat 8.5/9.0 最稳。
数据库要单独准备。用命令行或 Navicat 新建一个数据库,名字随意,比如baltika_db,然后把工程根目录里的baltika.sql导入进去。这张脚本里包含了建表语句和初始数据,导入完成后可以查一下有没有sys_user表,里面有默认的 admin 账号。环境准备阶段最容易出错的是 MySQL 8.0 的认证插件,老驱动连接时会报认证失败,后面避坑章节专门说。
4.2 修改配置:数据库连接与日志文件
数据库连接信息在resources/jdbc.properties里,打开后大概是下面这样:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/baltika_db?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456url 里有三个参数需要理解。useUnicode=true&characterEncoding=utf8保证写入数据库的中文不乱码;useSSL=false是关掉 MySQL 8.0 默认开启的 SSL,避免握手时报错;如果你的 MySQL 是 8.0,驱动要把com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver,并且 url 里还要加上serverTimezone=Asia/Shanghai指定时区,否则会报 CST 时区错误。
改完这四处,你的本地配置就算结束了。注意 jdbc.properties 最好不要提交到 Git,每个人的本机数据库密码不一样,我一般会把这个文件加进 .gitignore,再保留一个jdbc.properties.example作为模板,同事拉代码后复制改一下就能跑。
4.3 Maven 打包与 Tomcat 部署
在工程根目录执行打包命令。第一次跑的话 Maven 会下载大量依赖,建议在settings.xml里配置国内镜像源,否则可能等十分钟甚至报下载超时。命令本身很简单:
mvn clean package -DskipTestsclean是清空之前的编译产物,package是执行编译、测试、打包一整套流程,-DskipTests跳过测试代码,避免因为测试环境连不上数据库导致构建失败。打包完成后,target目录下会生成一个baltika.war文件。
部署方式有两种:一种是把 war 包复制到 Tomcat 的webapps目录,启动 Tomcat 后它会自动解压发布;另一种是在 IDEA 里配置 Tomcat,把打包产物作为 Artifact 部署。命令行方式适合验证,IDEA 方式适合开发调试。如果你是第一次用 IDEA 跑 war 工程,我建议先走命令行,确认环境没问题,再回 IDE 里配置调试,排查问题会少一个变量。
4.4 启动与验证:第一次看到页面的路径检查清单
启动 Tomcat 后,浏览器访问http://localhost:8080/baltika/login。这里容易踩一个坑:访问路径是否带/baltika,取决于 war 包文件名。如果打的包叫baltika.war,Tomcat 默认应用上下文就是/baltika;如果你把 war 改名成ROOT.war部署,那访问路径就直接是http://localhost:8080/。我一般保留原名,因为代码里如果写死了带 contextPath 的跳转,改名可能导致部分链接 404。
看到登录页后,用 SQL 脚本里的 admin 账号登录。如果登录后列表页数据正常显示、CSS 样式存在、点击翻页 URL 正常,说明整条链路通了。这一步验证的不只是页面,还包括数据库连接、MyBatis Mapper 映射、Spring 事务配置,任何一个环节有问题都会在登录动作上暴露出来。如果页面文字出来了但样式全丢,直接跳转到避坑章节的静态资源排查。
5. 常见问题与避坑:启动阶段最容易挂的六个点
跑老 Spring 工程的过程不会一帆风顺,问题通常集中在环境匹配和配置细节上。这一章把我在拆解和运行过程中遇到的高频问题列出来,每条按现象、原因、解决的顺序写。如果你在跑 baltika 时卡住了,先来这章找找有没有对应症状。
5.1 现象:Maven 依赖下载超时或卡在 0%
打包时日志停在 Downloading 状态一动不动,最后报超时。这是国内网络访问中央仓库太慢导致的,不是工程代码的问题。解决方式是打开 Maven 的settings.xml,在 mirrors 节点加一个国内镜像地址,把 central 仓库指过去。改完再执行mvn clean package,你会看到下载速度从几 KB 变成几 MB。
改 settings.xml 后最好加一个检查动作:执行mvn help:effective-settings,看当前生效的镜像配置是否正确。我遇到过同事改了 settings.xml 但配置文件写错了位置,Maven 根本没读它,白折腾半小时。另外,如果公司网络对镜像地址也不稳定,可以改用离线依赖导入,用 IDEA 的本地仓库直接指到前辈电脑里那份,但这种方案不通用,还是镜像最省心。
5.2 现象:Tomcat 能启动,但访问页面全部 404
Tomcat 日志显示启动成功,访问项目路径却 404。先别急着怀疑代码。第一个要确认的是访问的 contextPath 是不是和 war 包名一致,比如包名是baltika.war你访问了http://localhost:8080/,那必然 404。第二个要确认的是 IDEA 部署方式下有没有把依赖的库加进部署清单,老 Spring 工程如果没选 exploded war 的 lib 目录,Controller 类根本不会被加载。
还有一种隐蔽情况:Tomcat 启动了但 IDE 里没把 spring-mvc.xml 编译到 classes 目录。打开target/classes/spring目录看配置是否在,不在说明 resources 没有被识别为资源目录。解决方式是在 pom.xml 的 build 节点配置 resources,把src/main/resources显式声明为资源目录,重新打包再部署。
5.3 现象:连接数据库报时区或驱动类错误
启动时 Spring 创建数据源直接抛异常,提示Unknown initial character set index或者Unable to load authentication plugin。这两个症状本质上都是 MySQL 版本和驱动版本不匹配。MySQL 5.7 配老驱动能跑,但换成 MySQL 8.0 后,老驱动不认识新认证插件,必须换驱动。
按这样改:驱动类从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver,url 里加serverTimezone=Asia/Shanghai,如果你的 MySQL 时区本身有问题,还可以在连接串后面加useSSL=false&allowPublicKeyRetrieval=true。改完 jdbc.properties 重启 Tomcat 前,先在命令行用同样的参数测一下mysql -u root -p -h 127.0.0.1能不能连上,排除数据库端口没开、密码错误这类低级问题。
5.4 现象:页面正常出来但 CSS、JS 全部加载失败
浏览器 F12 打开控制台,静态资源请求全是 404。这个问题在 url-pattern 配成/的工程里是必修课。DispatcherServlet 接管了所有请求,包括 css、js、图片。spring-mvc.xml 里那行<mvc:resources mapping="/static/**" location="/static/"/>如果漏配,或者配置路径写错,静态资源就会全部打不进。
解决方式分两步:第一步确认 webapp/static 目录存在且里面有文件;第二步检查 spring-mvc.xml 的 mapping 属性是否和页面引用路径一致。页面里如果写的是/static/css/login.css,mapping 就必须是/static/**。改完配置重启 Tomcat,按 Ctrl+F5 强刷浏览器,避免缓存干扰判断。
5.5 现象:Tomcat 端口被占用,启动立刻失败
启动日志出现Port 8080 was already in use,说明上一个 Tomcat 实例没有完全关闭,或者别的进程占用了端口。解决方式不是改个 8081 就完事,而是先找出占用进程。Linux 上用netstat -tlnp | grep 8080,Windows 上用netstat -ano | findstr 8080,确认占用者的 PID,判断是不是残留的 Java 进程。
如果确认是残留的 Tomcat 进程,直接结束它再重启;如果不是,才考虑修改 Tomcat 的server.xml里两个端口:HTTP 端口8080和关闭端口8005。我个人的习惯是不改默认端口,因为代码里如果有写死跳转地址的配置,换端口会引发一堆连带问题,不如先把残留进程清理干净。每次重启前用sh shutdown.sh而不是直接杀进程,这能避免很多莫名其妙的资源锁问题。
6. 二次开发实战:给系统增加公告模块和 JSON 接口
跑通只是第一步,拿到源码不改造总是缺了点乐趣。这章演示一个完整的二次开发流程:加一个公告管理模块,同时把接口改造成返回 JSON,方便前后端分离。这套思路可以直接迁到你自己的模块上。
6.1 新增业务模块的落地顺序
在 baltika 现有骨架里加模块,顺序固定是:建表 → 建实体 → 写 Mapper 接口和 XML → 写 Service接口和实现 → 写 Controller → 加页面。比如加公告表,SQL 脚本里加一张sys_notice,字段包含 id、title、content、create_time。实体类对应建好后,Mapper 里写selectAll和insert两个方法,XML 里补 SQL。Service 层把 Mapper 包一层,Controller 里写好接口后,在WEB-INF/views/notice目录下新建list.jsp,一个模块就成型了。
唯一需要额外关注的是事务配置。老工程的事务一般在 spring-context.xml 里通过tx:advice配置,拦截 service 包下所有方法。新加的 Service 只要放在同一个包路径结构下,不需要额外配置就能自动加入事务,这是组件扫描的便利之处。但如果你把包名改了,扫描不到,事务和 Bean 都不会生效,改包名前先检查扫描注解的 base-package 属性。
6.2 把列表接口改造成 JSON 返回并验证
默认的 Controller 返回视图名,如果需要纯数据接口,有两种改法。局部改法是在方法上加@ResponseBody,让 Spring 直接序列化返回值;彻底做法是新建一个@RestController,所有方法默认返回 JSON。我推荐后者,和旧代码共存时不影响原逻辑。
@RestController @RequestMapping("/api/notice") public class NoticeApiController { @Autowired private NoticeService noticeService; @RequestMapping("/list") public Result<List<Notice>> list() { return Result.success(noticeService.selectAll()); } }代码里的Result是统一响应体,包含 code、message、data 三个字段,这是前后端分离时常用的封装方式。成功时 code 为 0,失败时为非 0,前端只需要判断 code 就能弹出统一错误提示。改完重启后,用 Postman 访问http://localhost:8080/baltika/api/notice/list,返回 JSON 里能看到公告列表就说明接口通了。如果报 406 或返回乱码,检查 spring-mvc.xml 里有没有配置 Jackson 的MappingJackson2HttpMessageConverter和 UTF-8 编码过滤器,老工程经常漏这一步。
从那以后,我每次拿到陌生源码,都会先看 web.xml 和 spring-mvc.xml,把请求入口和视图解析器读通再动业务代码。baltika 让这个习惯牢固地保持到了现在,也让我少走不少弯路,希望帮到你。
本文还有配套的精品资源,点击获取