简介:这份资源是面向JavaWeb初学者与课程设计学习者的完整项目包,围绕Servlet、Bootstrap、Ajax与JSP技术栈,实现一个功能简洁的个人博客系统,适合正在准备课程设计或希望串联前后端知识点的开发者参考。压缩包共2个文件,包含1个zip项目源码包与1个sql数据库脚本,整体约4.12MB,源码包内按标准JavaWeb目录组织,数据库脚本可直接导入以还原博客文章、用户与评论等基础数据。项目覆盖Servlet请求处理、JSP视图渲染、Ajax无刷新评论、Bootstrap响应式页面以及JDBC数据库操作等核心环节,并体现MVC分层与session用户认证思路,便于读者理解各技术在实际场景中的协作方式。目前已有282人学习,可作为课程设计参考或JavaWeb入门练手素材,帮助快速搭建可运行的博客原型并梳理项目结构。
1. 从零搭一个 Javaweb 个人博客:servlet+bootstrap+ajax+jsp 到底能做出什么
很多人做 Javaweb 课程设计,第一反应是去搜「javaweb项目完整案例mysql」,结果下下来一堆跑不起来的压缩包,Tomcat 一启动就是 404 或者 500。其实用 servlet + bootstrap + ajax + jsp 这套最经典的组合,做一个能发文章、能看列表、能登录的个人博客,代码量并不大,难点在于把请求链路和页面渲染这两件事串明白。这套方案适合两类人:一是要交课程设计、需要一套自己能讲清楚原理的完整项目;二是刚学完 servlet 想找个东西练手,把 JSP 展示、AJAX 异步提交、Bootstrap 排版一次性打通。它不追求高并发,也不上 Spring 全家桶,就是把最原始的 HTTP 请求响应模型走一遍,你做完之后再看任何框架都会觉得眼熟。下面我按实际开发顺序,从环境配置讲到分页查询和异步删除,把每一步的命令、参数和翻车点都摊开说。
2. 环境与项目骨架:IDEA 里跑通第一个 servlet 的最小配置
2.1 为什么选 Tomcat 9 而不是 10,以及 JDK 版本怎么定
这套技术栈里最容易出玄学问题的地方就是版本。servlet 的包名在 Tomcat 10 之后从javax.servlet变成了jakarta.servlet,如果你照着老教程写import javax.servlet.http.HttpServlet,在 Tomcat 10 上编译直接报「程序包 javax.servlet 不存在」。课程设计阶段没必要折腾新命名空间,直接锁 Tomcat 9.x + JDK 8 或 11,网上绝大多数资料都能对上。JDK 选 8 是因为很多学校的实验环境还是 8,选 11 也可以,但别用 17 以上,部分老版本 IDEA 配合 Tomcat 9 会有模块访问警告。
在 IDEA 里新建项目时选「Java Enterprise」,勾选「Web Application」,它会自动生成web目录和WEB-INF/web.xml。如果你用的是社区版 IDEA,没有这个选项,就建普通 Java 项目,手动在项目结构里添加 Web Facet,再配 Artifact。手动配的时候注意:web目录要放在项目根下,WEB-INF里放web.xml和lib,编译输出目录设成web/WEB-INF/classes,否则 Tomcat 找不到 class 文件。
依赖方面,只需要两个 jar:servlet-api.jar(从 Tomcat 的 lib 目录拷,或者用 Maven 坐标javax.servlet:javax.servlet-api:4.0.1,scope 设 provided)和mysql-connector-java(8.x 版本,对应 MySQL 8)。如果你用 Maven,pom.xml里加这两条就够了,别引一堆用不上的包,课程设计答辩时老师问起来你也不好解释。
<!-- pom.xml 关键依赖,scope 一定要写对 --> <dependencies> <!-- servlet-api 由 Tomcat 提供,打包时不带进去,否则冲突 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- MySQL 8 驱动,注意时区参数在 URL 里配 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>参数说明:scope写provided是因为 Tomcat 自带 servlet-api,如果你打成 war 包时把它带进去,启动会报类加载冲突。MySQL 驱动版本要和你的数据库版本匹配,8.0 的库用 5.x 驱动连会报「Unknown system variable 'query_cache_size'」。
2.2 数据库建表和连接池的最小写法
博客至少需要三张表:用户表、文章表、分类表(可选)。课程设计为了简单,可以只做用户和文章两张。建表语句如下,注意字符集用utf8mb4,否则 emoji 存进去会变问号。
-- 用户表:存登录信息 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, -- 实际项目要加盐哈希,课设可先明文 `nickname` VARCHAR(50) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 文章表:存博客正文 CREATE TABLE `article` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `content` TEXT, `author_id` INT, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`author_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;连接数据库我一般写一个DBUtil工具类,用DriverManager直接拿连接,课设够用。如果你想让代码好看点,可以引 Druid 或 HikariCP,但多一个 jar 就多一个配置项,答辩时被问到连接池参数答不上来反而尴尬。DBUtil里把 URL、用户名、密码写成常量,URL 里必须带useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,少一个时区参数就可能报「The server time zone value is unrecognized」。
// DBUtil.java 最简连接获取 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/blog?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "123456"; public static Connection getConn() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 8.x 驱动类名带 cj } catch (ClassNotFoundException e) { e.printStackTrace(); } return DriverManager.getConnection(URL, USER, PWD); } }逻辑说明:Class.forName加载驱动,MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,写老版的com.mysql.jdbc.Driver会提示已过时但仍能连。每次getConn都新建连接,课设访问量小没问题,但记得在 DAO 里用 try-with-resources 关闭,否则连接数一多数据库就拒绝服务。
3. Servlet 处理请求与 JSP 渲染:把文章列表和详情页跑通
3.1 一个 Servlet 同时处理列表和详情的写法
Servlet 的映射可以用@WebServlet注解,也可以用web.xml。课设推荐注解,少写配置文件。比如文章模块,我习惯用一个ArticleServlet,通过参数action区分操作:list查列表、detail查详情、add发文章、delete删文章。这样比每个功能写一个 Servlet 类要清爽,也方便在前端统一请求路径。
@WebServlet("/article") public class ArticleServlet extends HttpServlet { private ArticleDao articleDao = new ArticleDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("list".equals(action)) { // 查所有文章,转发到列表页 List<Article> list = articleDao.findAll(); req.setAttribute("articles", list); req.getRequestDispatcher("/list.jsp").forward(req, resp); } else if ("detail".equals(action)) { // 根据 id 查单篇 String id = req.getParameter("id"); Article article = articleDao.findById(Integer.parseInt(id)); req.setAttribute("article", article); req.getRequestDispatcher("/detail.jsp").forward(req, resp); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置编码,必须在取参数之前 req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("add".equals(action)) { String title = req.getParameter("title"); String content = req.getParameter("content"); articleDao.insert(new Article(title, content, 1)); resp.sendRedirect("article?action=list"); // 重定向回列表 } } }参数说明:req.setCharacterEncoding("UTF-8")只对 POST 请求体有效,GET 请求的中文乱码要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8",或者干脆用 POST。forward是服务器内部跳转,URL 不变;sendRedirect是让浏览器重新请求,URL 会变。发完文章用重定向是为了防止刷新页面重复提交。
3.2 JSP 里用 JSTL 循环展示文章列表
JSP 里不要写 Java 脚本片段(<% %>),用 JSTL 和 EL 表达式,页面干净,老师看着也舒服。需要引入jstl.jar和standard.jar,或者 Maven 坐标javax.servlet:jstl:1.2。列表页核心代码:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table class="table table-hover"> <thead> <tr><th>标题</th><th>作者</th><th>时间</th><th>操作</th></tr> </thead> <tbody> <c:forEach items="${articles}" var="a"> <tr> <td><a href="article?action=detail&id=${a.id}">${a.title}</a></td> <td>${a.authorName}</td> <td>${a.createTime}</td> <td><button class="btn btn-danger btn-sm" onclick="del(${a.id})">删除</button></td> </tr> </c:forEach> </tbody> </table>逻辑说明:<c:forEach>遍历 request 域里的articles,${a.id}是 EL 表达式,会自动调用 getter。注意items里的属性名要和req.setAttribute的 key 一致。如果列表为空,页面什么都不显示,最好加一个<c:if test="${empty articles}">提示「暂无文章」。
3.3 Bootstrap 排版:用 CDN 还是本地文件
Bootstrap 用 5.x 还是 4.x 都行,课设推荐 5.x,栅格类名和 4 差不多,但去掉了 jQuery 依赖。引入方式有两种:CDN 和本地。学校机房可能断网,建议下载到web/static目录下本地引入,答辩演示时不怕网络问题。
<!-- 在 JSP 的 head 里引入本地 Bootstrap --> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/bootstrap.min.css"> <script src="${pageContext.request.contextPath}/static/js/bootstrap.bundle.min.js"></script>参数说明:${pageContext.request.contextPath}拿到项目上下文路径,避免项目名变了之后路径 404。Bootstrap 的container、row、col-md-8这套栅格用来做博客布局很顺手,左边文章列表占 8 列,右边个人信息占 4 列,响应式在手机上会自动堆叠。
4. AJAX 异步交互:删除、登录校验和编码格式的坑
4.1 用原生 fetch 发 AJAX 请求删除文章
AJAX 的作用是不刷新页面就能删文章、校验用户名。jQuery 的$.ajax用的人多,但 Bootstrap 5 已经不依赖 jQuery 了,我建议直接用原生fetch,代码更短,也不用额外引库。
// 删除文章,不刷新页面 function del(id) { if (!confirm('确定删除?')) return; fetch('article?action=delete&id=' + id, { method: 'POST' }) .then(resp => resp.text()) .then(result => { if (result === 'ok') { // 删除成功,把当前行移除 document.querySelector(`button[onclick="del(${id})"]`).closest('tr').remove(); } else { alert('删除失败'); } }) .catch(err => console.error('请求出错', err)); }逻辑说明:fetch第一个参数是 URL,第二个参数是配置对象。这里用 POST 传 id,Servlet 里doPost处理delete动作,删除成功后返回字符串ok。前端拿到ok后用 DOM 操作移除表格行,页面不刷新。注意fetch默认不带 cookie,如果登录态用 session 存,需要加credentials: 'same-origin'。
4.2 AJAX 请求参数编码:POST 和 GET 的编码差异
热搜里有人问「ajax请求设置编码格式」,这是真踩过坑才会问的。AJAX 发 POST 请求时,如果请求头是application/x-www-form-urlencoded,中文参数要在服务端用req.setCharacterEncoding("UTF-8")解码。如果发的是 JSON,请求头设Content-Type: application/json,Servlet 里就得用流读取再手动解析,课设阶段不推荐,直接表单编码最简单。
// 登录校验,POST 表单编码 fetch('user?action=check', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=' + encodeURIComponent(name) })参数说明:encodeURIComponent对参数值编码,防止特殊字符截断。服务端req.getParameter("username")拿到的就是解码后的值。如果发现中文变乱码,先检查 Tomcat 的server.xml里 Connector 有没有URIEncoding="UTF-8",再检查 Servlet 里setCharacterEncoding是不是在getParameter之前调用。
4.3 登录拦截:用 Filter 还是每个 Servlet 里判断
博客的后台发文章、删除文章需要登录才能操作。最省事的做法是写一个LoginFilter,拦截/article和/admin/*路径,检查 session 里有没有用户。没登录就重定向到登录页。Filter 的@WebFilter注解里urlPatterns可以写多个。
@WebFilter(urlPatterns = {"/article", "/admin/*"}) public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { chain.doFilter(req, resp); // 放行 } } }逻辑说明:chain.doFilter是放行到下一个过滤器或目标 Servlet。注意登录页和登录请求本身不能被拦截,否则会死循环。urlPatterns里不要包含/login和/user。
5. 避坑与排查:课程设计里最容易翻车的 5 个地方
5.1 现象:Tomcat 启动报 404,访问任何页面都是「未找到」
原因:IDEA 的 Artifact 没配好,WEB-INF/classes下没有编译后的 class 文件,或者web.xml里metadata-complete="true"导致注解不生效。解决:打开 Project Structure → Artifacts,确认 Output Layout 里WEB-INF/classes指向了编译输出目录;web.xml的web-app标签里不要写metadata-complete="true",否则@WebServlet会被忽略。
5.2 现象:JSP 页面中文乱码,浏览器显示问号
原因:JSP 文件本身编码不是 UTF-8,或者响应头没设charset。解决:在 JSP 第一行加<%@ page contentType="text/html;charset=UTF-8" language="java" %>,同时 IDEA 的 File Encoding 设成 UTF-8。数据库连接 URL 里加characterEncoding=utf8,建表用utf8mb4。
5.3 现象:AJAX 请求返回 200 但前端拿不到数据
原因:Servlet 返回的是转发到 JSP 的 HTML,而 AJAX 期望的是纯文本或 JSON。解决:AJAX 处理的 Servlet 分支里不要用forward,直接用resp.getWriter().write("ok")输出。如果返回 JSON,设resp.setContentType("application/json;charset=UTF-8"),手动拼 JSON 字符串或用 Gson。
5.4 现象:删除文章后刷新页面,数据又回来了
原因:删除操作后用了forward而不是sendRedirect,浏览器地址栏还是删除请求的 URL,刷新就重复提交。解决:增删改操作完成后一律resp.sendRedirect("article?action=list"),让浏览器重新发一次 GET 请求。
5.5 现象:MySQL 连接报「Public Key Retrieval is not allowed」
原因:MySQL 8 默认用caching_sha2_password认证插件,JDBC 连接时不允许公钥检索。解决:在 URL 后面加allowPublicKeyRetrieval=true,或者把用户的认证插件改成mysql_native_password。课设环境直接加参数最快。
6. 分页查询与异步加载:把博客列表做成能翻页的进阶技巧
列表文章一多,全查出来页面会很长,分页是课程设计里能加分的点。分页的核心是 SQL 的LIMIT和COUNT。先查总数,再按页码算偏移量。我一般写一个PageBean<T>泛型类,装当前页数据、总页数、当前页码。
// 分页查询 DAO 写法 public PageBean<Article> findByPage(int pageNo, int pageSize) { PageBean<Article> pb = new PageBean<>(); pb.setPageNo(pageNo); pb.setPageSize(pageSize); // 查总数 String countSql = "SELECT COUNT(*) FROM article"; // 查当前页数据,偏移量 = (页码-1)*每页条数 String dataSql = "SELECT * FROM article ORDER BY create_time DESC LIMIT ?, ?"; // ... JDBC 执行,setInt(1, (pageNo-1)*pageSize), setInt(2, pageSize) return pb; }参数说明:LIMIT第一个参数是偏移量,从 0 开始;第二个参数是每页条数。页码从 1 开始传,偏移量就是(pageNo-1)*pageSize。如果页码超过总页数,返回空列表,前端显示「没有更多了」。
前端分页可以用 AJAX 异步加载,点「下一页」时不刷新整个页面,只替换表格内容。这样体验更接近现代博客。实现思路:把文章列表抽成一个list.jsp片段,AJAX 请求article?action=list&pageNo=2返回这段 HTML,前端用innerHTML替换。注意返回的片段不要带<html>和<body>标签,否则嵌套会乱。
// 异步翻页 function loadPage(pageNo) { fetch('article?action=list&pageNo=' + pageNo) .then(resp => resp.text()) .then(html => { document.getElementById('articleTable').innerHTML = html; }); }这里有个血泪经验:AJAX 返回的 HTML 片段里如果包含<script>标签,innerHTML插入后脚本不会执行。所以删除按钮的onclick要写在标签属性里,或者用事件委托绑在父元素上。我一般用事件委托,在articleTable上监听 click,判断event.target是不是删除按钮。
最后说一个验证方法:分页做完后,手动在地址栏输pageNo=999,看是否返回空列表而不是报错;再输pageNo=0或负数,看有没有做边界判断。这两个测试能过,分页逻辑基本就没问题。我自己做课设时习惯把每页条数设成 5,方便演示翻页效果,数据量小也能看出页码变化。希望帮到你。
本文还有配套的精品资源,点击获取