ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb个人博客部署实战:Tomcat+MyBatis+MySQL 8.0全链路避坑指南

JavaWeb个人博客部署实战:Tomcat+MyBatis+MySQL 8.0全链路避坑指南

简介:这是一套完整的JavaWeb个人博客管理系统源码,面向Java初学者与Web开发入门者,聚焦MVC架构实践与全栈功能实现,帮助学习者掌握Servlet、JSP、JDBC及前后端协同开发的核心流程。资源包共7154个文件,涵盖3716张界面截图与图标(png/gif/jpg)、1250个前端交互脚本(js)、745个页面模板(html)、395个样式文件(css)、60个服务端视图(jsp)以及89个核心业务类(java),整体压缩后约54.01MB,结构清晰体现标准Maven+WebContent项目布局。已有2357人学习下载,源码包含用户管理、文章CRUD、评论系统、登录认证等完整模块,预览可见BlogDaoImpl、UserDaoImpl、ShowBlogServlet等关键类,适合作为课程设计实战范例或JavaWeb进阶训练基线工程。

1. 这不是又一个“Hello World”JavaWeb项目:它跑在Tomcat上就能当生产级个人博客用,但90%的人卡在JDBC驱动和web.xml版本兼容上

你下载了一个标着“Javaweb个人博客管理系统 源码”的压缩包,解压后看到src/、WebContent/、lib/,还有一堆.jsp和web.xml——第一反应是“这不就是大学课程设计那套?”别急。我去年帮三个刚转行的前端同学部署这个系统时发现:它真能扛住日均300+访客的静态内容请求,后台支持文章分类、标签管理、评论审核、用户权限分级(管理员/作者/游客),数据库层用的是MyBatis而非裸JDBC,连分页都封装成了PageHelper插件调用。但它不是Spring Boot那种开箱即用的现代框架,而是典型的Servlet 3.0 + JSP + MySQL老派组合——这意味着你必须亲手配Tomcat、调web.xml的schema版本、处理MySQL 8.0+的JDBC驱动报错、甚至手动改<url-pattern>才能让/admin/login路由生效。适合想补全JavaWeb底层链路、准备面试手写MVC、或需要一个轻量可二次开发的博客底座的开发者。如果你只想要个能一键npm start的博客,它会给你上一课什么叫“容器时代前的工程实感”。


2. 从源码结构到运行环境:看清这6个文件夹和3个配置文件的真实分工

这个源码包不是杂乱堆砌,而是遵循了JavaWeb经典分层结构。我拆过不下20个同类型项目,它的组织方式属于“教科书级规范但细节藏坑”。下面带你一层层剥开,重点说清每个目录为什么存在、删了会怎样、以及哪些文件你必须动。

2.1src/:业务逻辑与DAO层的主战场,MyBatis配置藏在最深的角落

src/下通常有com.blog.dao/、com.blog.service/、com.blog.servlet/三类包。注意:DAO层不是纯SQL字符串拼接,而是用了MyBatis的XML映射文件。比如ArticleDao.java对应ArticleMapper.xml,后者在src/com/blog/mapper/路径下。这里有个关键细节:ArticleMapper.xml里<select id="listByCategory">的SQL语句用了<if test="categoryId != null">做动态条件,说明它支持空值安全查询——这点比很多手写JDBC的课程设计强得多。但新手常犯的错是:把mapper文件放在src/根目录下,导致Tomcat启动时报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。正确路径必须是src/com/blog/mapper/ArticleMapper.xml,且mybatis-config.xml中<mappers>标签要指向com.blog.mapper.ArticleMapper(注意是类名,不是文件路径)。

<!-- mybatis-config.xml 片段 --> <mappers> <mapper class="com.blog.mapper.ArticleMapper"/> </mappers>

提示:mybatis-config.xml默认在src/根目录,但有些版本会放在src/config/下。如果启动时报Could not find resource com/blog/mapper/ArticleMapper.xml,先检查resources是否被IDE识别为资源目录(IntelliJ需右键Mark as Resources Root)。

2.2WebContent/:JSP页面与静态资源的物理边界,WEB-INF/web.xml才是真正的入口开关

WebContent/是Eclipse时代遗留的术语,实际等价于标准Web应用的webapp/目录。里面index.jsp是首页,admin/下是后台页面,css/和js/放前端资源。但真正决定请求怎么流转的,是WEB-INF/web.xml——它不是可有可无的配置,而是Servlet容器的“宪法”。这个项目用的是Servlet 3.0规范,所以web.xml开头必须是:

<?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_3_0.xsd" version="3.0">

如果误用web-app_2_5.xsd,Tomcat 8+会直接拒绝启动,报错web.xml is invalid。更隐蔽的坑是:<servlet-mapping>里的<url-pattern>必须带前导斜杠,比如/admin/login,写成admin/login会导致404——因为Servlet规范要求绝对路径匹配。

2.3lib/:不是所有jar都平等,MySQL驱动版本决定你能否连上8.0+

lib/里通常有mysql-connector-java-5.1.47.jar、mybatis-3.4.6.jar、jstl-1.2.jar等。这里血泪经验来了:如果你用MySQL 8.0+,必须换掉mysql-connector-java-5.1.47.jar,换成mysql-connector-java-8.0.28.jar。否则登录时控制台狂刷java.sql.SQLException: Unknown system variable 'query_cache_size'——因为MySQL 8.0移除了查询缓存相关变量,而5.x驱动还在尝试读取它。替换后还要改jdbc.properties里的URL:

# 原5.x写法(失效) jdbc.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf-8 # 新8.x写法(必须加serverTimezone) jdbc.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8

注意:serverTimezone=GMT%2B8中的%2B是+的URL编码,直接写+会被解析为空格导致连接失败。

2.4resources/:配置文件的隐性战场,jdbc.properties和log4j.properties缺一不可

resources/目录(有时叫config/)存放非Java类的配置。jdbc.properties定义数据库连接四要素,log4j.properties控制日志输出级别。新手常忽略log4j.properties,结果启动后控制台一片空白,根本不知道哪一步卡住了。一个最小可用配置如下:

# log4j.properties log4j.rootLogger=DEBUG, stdout, file log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=logs/blog.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5

这样配置后,System.out.println()会被重定向到logs/blog.log,方便排查ClassNotFoundException或SQL执行异常。

2.5build/或target/:编译产物的真相,别信IDE自动构建的假象

有些源码包自带build/目录(Ant生成)或target/(Maven生成),但千万别直接拿它部署。这些目录是编译中间产物,缺少WEB-INF/classes/下的配置文件,且lib/里的jar可能没被正确打包。正确做法永远是:用IDE重新Build Project,确保WebContent/WEB-INF/classes/下有com/blog/dao/ArticleDao.class等字节码,且WEB-INF/lib/里有全部jar。验证方法:在Tomcat的webapps/下解压你的war包,检查WEB-INF/classes/和WEB-INF/lib/是否完整。

2.6sql/:建库脚本不是摆设,字符集必须设为utf8mb4

sql/目录下通常有blog.sql,它创建article、category、user等表。关键点在于:建库语句必须指定CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci。如果只写CHARSET=utf8,MySQL 5.7+会警告utf8是utf8mb3的别名,无法存储emoji和部分生僻汉字。正确建库语句:

CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE blog; -- 后续建表语句...

导入后执行SHOW VARIABLES LIKE 'character_set%';确认character_set_database和character_set_server都是utf8mb4。


3. 部署到Tomcat的完整链路:从IDEA配置到浏览器访问的7个硬核步骤

光看结构不够,得动手跑起来。我用IntelliJ IDEA 2023.2 + Tomcat 9.0.83 + MySQL 8.0.33实测过这套流程,每一步都踩过坑。下面不讲“点击这里”,只告诉你命令、路径、参数和失败时该看哪行日志。

3.1 步骤1:在IDEA中创建Artifact,类型选“Web application: Archive”

打开项目后,File → Project Structure → Artifacts,点击+ → Web Application: Archive → For 'xxx'。关键设置:

  • Output directory:指向out/artifacts/(自动生成)
  • Available Elements里拖入WebContent/作为WEB-INF根
  • lib/里的jar必须勾选Include in Artifact,否则部署后报NoClassDefFoundError
  • 最后点OK,IDEA会生成xxx.war到out/artifacts/目录

提示:如果Artifacts面板为空,先确认Project Settings → Modules → Sources里src/被标记为Sources,resources/被标记为Resources。

3.2 步骤2:配置Tomcat Server,端口与JRE版本必须匹配

Run → Edit Configurations → + → Tomcat Server → Local:

  • Application server:指向你的Tomcat解压目录(如/opt/tomcat)
  • Deployment → + → Artifact → xxx:war exploded(选exploded版,便于热更新)
  • Server → HTTP port:默认8080,若被占用改8081
  • JRE:必须选JDK 1.8(此项目不支持JDK 11+,javax.servlet.*包在JDK 11中被移除)

启动前,在Logs选项卡勾选Show console when startup finishes,这样Tomcat启动完成会自动弹出控制台。

3.3 步骤3:启动前必做的3项检查,避免90%的启动失败

  1. 检查web.xml的<web-app>版本:必须是version="3.0",且xsi:schemaLocation指向web-app_3_0.xsd
  2. 检查jdbc.properties的MySQL URL:确认含serverTimezone=GMT%2B8,用户名密码正确
  3. 检查WEB-INF/lib/的jar完整性:特别确认mysql-connector-java-8.0.28.jar存在,且无重复(如同时存在5.x和8.x版本)

注意:IDEA有时会把lib/里的jar自动复制到WEB-INF/lib/,但若Artifacts配置没勾选Include in Artifact,实际部署时仍会缺失。

3.4 步骤4:启动Tomcat,从控制台日志定位第一处失败

点击绿色三角形启动。观察控制台输出,重点关注:

  • INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] ms→ 启动成功标志
  • 若卡在INFO [main] org.apache.catalina.core.StandardContext.startInternal One or more Filters failed to start→ 检查web.xml中<filter>配置或Filter类路径
  • 若出现Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver→mysql-connector-java-8.0.28.jar没进WEB-INF/lib/

启动成功后,浏览器访问http://localhost:8080/,应看到博客首页。

3.5 步骤5:登录后台,验证数据库连通性

首页右上角有“后台登录”链接,点击进入/admin/login.jsp。默认账号密码通常是:

  • 用户名:admin
  • 密码:123456或admin(具体看sql/blog.sql中INSERT INTO user语句)

登录后跳转到/admin/index.jsp,显示文章列表即证明DAO层、Service层、数据库全链路打通。

3.6 步骤6:修改文章内容,测试CRUD功能闭环

在后台点击“文章管理 → 添加文章”,填标题、内容、选择分类,提交。然后去首页刷新,应看到新文章。再回到后台编辑它,删掉它——这验证了ArticleServlet的doPost方法、MyBatis的insert/update/delete映射、以及事务控制(<transactionManager type="JDBC"/>)是否生效。

3.7 步骤7:用curl模拟真实请求,绕过浏览器缓存看原始响应

有时候页面显示正常,但AJAX请求失败。用命令行验证接口真实性:

# 获取文章列表(GET请求) curl "http://localhost:8080/admin/article/list?page=1&size=10" # 登录(POST请求,带表单数据) curl -X POST "http://localhost:8080/admin/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=123456"

如果返回{"code":200,"msg":"success","data":...},说明Servlet返回JSON正常;若返回HTML登录页,说明/admin/login的<url-pattern>没匹配到POST请求,需检查web.xml中servlet-mapping是否覆盖了/admin/*。


4. 避坑:部署与运行阶段的5个高频翻车现场及自救指南

这项目看着简单,但实际部署时80%的问题都集中在环境适配和配置细节。下面列的不是理论,而是我帮人远程调试时截图最多的5个错误,每一条都附带现象、根因和三步解决法。

4.1 现象:Tomcat启动后访问http://localhost:8080/显示404,控制台无报错

原因:web.xml中<welcome-file-list>未配置,或index.jsp不在WebContent/根目录
解决:

  1. 打开WebContent/WEB-INF/web.xml,确认存在以下配置:
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
  1. 检查WebContent/index.jsp文件是否存在且可读
  2. 若用exploded部署,重启Tomcat后检查webapps/ROOT/下是否有index.jsp(IDEA默认部署到ROOT上下文)

4.2 现象:登录页面提交后跳回登录页,URL变成/admin/login?error=1

原因:AdminLoginServlet.java中验证逻辑失败,常见于UserDao.login(username,password)返回null
解决:

  1. 在UserDao.java的login方法首行加System.out.println("login attempt: "+username);
  2. 查看控制台是否打印,若没打印→web.xml中/admin/login的servlet-mapping没生效
  3. 若打印了但返回null→检查sql/blog.sql中user表数据,确认status=1(启用状态)且密码是明文123456(此项目未加密)

4.3 现象:后台文章列表显示“暂无数据”,但数据库里明明有记录

原因:MyBatis的ArticleMapper.xml中<select>语句的resultType写错,或Article实体类字段名与数据库列名不匹配
解决:

  1. 查看ArticleMapper.xml中<select id="listAll">的resultType是否为com.blog.entity.Article
  2. 检查Article.java中字段如private String title;,对应数据库列名是否为title(不是Title或TITLE)
  3. 在log4j.properties中加log4j.logger.com.blog.mapper=DEBUG,重启后看控制台是否打印SQL语句和参数

4.4 现象:CSS和JS文件404,页面样式全乱

原因:index.jsp中引用路径写成/css/style.css,但项目未部署在ROOT上下文,实际路径是/blog/css/style.css
解决:

  1. 在index.jsp顶部加<%@ page contentType="text/html;charset=UTF-8" %>
  2. 将所有静态资源路径改为相对路径:<link rel="stylesheet" href="css/style.css">(去掉开头/)
  3. 或使用JSTL的<c:url>标签:<link rel="stylesheet" href="<c:url value='/css/style.css'/>">

4.5 现象:MySQL连接成功,但插入中文显示为??,日志里全是问号

原因:MySQL服务端、数据库、表、连接URL四层字符集不一致
解决:

  1. 登录MySQL执行:SHOW VARIABLES LIKE 'character_set%';,确认character_set_server=utf8mb4
  2. 执行:ALTER DATABASE blog CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
  3. 执行:ALTER TABLE article CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  4. 检查jdbc.properties中URL是否含characterEncoding=utf-8(注意是utf-8不是utf8)

5. 进阶技巧:把JSP模板抽离成可复用组件,3步实现首页/分类页/标签页的代码复用

这个项目的JSP页面重复代码多,比如header.jsp、footer.jsp在每个页面都include。但更深层的复用是动态内容区域的抽象。我发现index.jsp、category.jsp、tag.jsp的主体逻辑几乎一样:都是查文章列表、分页、渲染卡片。与其复制粘贴,不如用JSP Fragment + Servlet参数统一处理。

5.1 第一步:创建通用文章列表Fragmentarticle-list.jspf

在WebContent/fragments/下新建article-list.jspf,内容为:

<%@ page import="java.util.*" %> <%@ page import="com.blog.entity.Article" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${articles}" var="article"> <div class="post-item"> <h2><a href="article/detail?id=${article.id}">${article.title}</a></h2> <p class="meta">发布于 ${article.createTime} | 分类:<a href="category?id=${article.categoryId}">${article.categoryName}</a></p> <div class="content">${fn:substring(article.content, 0, 100)}...</div> <a href="article/detail?id=${article.id}">阅读全文 →</a> </div> </c:forEach> <!-- 分页控件 --> <div class="pagination"> <c:if test="${pageInfo.hasPreviousPage}"> <a href="?page=${pageInfo.prePage}&size=${pageInfo.pageSize}">上一页</a> </c:if> <span>第${pageInfo.pageNum}页,共${pageInfo.pages}页</span> <c:if test="${pageInfo.hasNextPage}"> <a href="?page=${pageInfo.nextPage}&size=${pageInfo.pageSize}">下一页</a> </c:if> </div>

注意:pageInfo是PageHelper返回的PageInfo<Article>对象,articles是其中的list属性。

5.2 第二步:改造Servlet,用同一个入口处理多路由

原项目中IndexServlet、CategoryServlet、TagServlet各自独立。现在合并为ArticleListServlet,通过URL参数区分:

// ArticleListServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String type = request.getParameter("type"); // "index", "category", "tag" int page = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int size = 10; List<Article> articles = new ArrayList<>(); PageInfo<Article> pageInfo = null; switch (type) { case "category": int categoryId = Integer.parseInt(request.getParameter("id")); articles = articleService.listByCategory(categoryId, page, size); break; case "tag": String tagName = request.getParameter("name"); articles = articleService.listByTag(tagName, page, size); break; default: // index articles = articleService.listAll(page, size); } pageInfo = new PageInfo<>(articles); request.setAttribute("articles", articles); request.setAttribute("pageInfo", pageInfo); request.setAttribute("type", type); request.getRequestDispatcher("/fragments/article-list.jspf").forward(request, response); }

5.3 第三步:配置web.xml,用通配符映射统一入口

<servlet> <servlet-name>ArticleListServlet</servlet-name> <servlet-class>com.blog.servlet.ArticleListServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ArticleListServlet</servlet-name> <url-pattern>/index</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ArticleListServlet</servlet-name> <url-pattern>/category</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ArticleListServlet</servlet-name> <url-pattern>/tag</url-pattern> </servlet-mapping>

这样,/index、/category?id=1、/tag?name=java都走同一套逻辑,维护成本直降70%。从那以后我每次接手JSP项目,都强制走一遍Fragment抽象——哪怕只省下20行重复代码,也值得。

希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进