news 2026/10/8 3:19:36

JavaWeb个人博客实战:Servlet+Bootstrap+Ajax+JSP完整骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb个人博客实战:Servlet+Bootstrap+Ajax+JSP完整骨架

简介:本资源是一份面向JavaWeb初学者与课程设计实践者的完整个人博客系统开发案例,聚焦Servlet、Bootstrap、Ajax与JSP四大核心技术的协同应用,帮助学习者掌握Web前后端交互、响应式界面开发及动态内容渲染等关键能力。压缩包共2个文件(1个ZIP源码包+1个SQL数据库脚本),总大小4.12MB;ZIP包内含标准JavaWeb项目结构(含WEB-INF、src、lib等目录),涵盖用户登录、博客发布、评论提交等核心功能模块,SQL脚本用于初始化MySQL数据库表结构与基础数据。已有282人学习下载,适合高校JavaWeb课程实训、期末项目参考或自学进阶使用。读者可直接部署运行,深入理解MVC分层实现、Session会话管理、EL/JSTL页面表达、Ajax无刷新交互及Bootstrap响应式布局等实战要点,同时获得清晰的工程组织范例与可调试的完整代码链路。

1. 这不是“又一个JSP页面”,而是一套能跑通、能调试、能交作业、还能当简历项目的JavaWeb个人博客骨架

你手头正开着IDEA,刚建好一个Dynamic Web Project,却卡在web.xml里配了三遍<servlet-mapping>还是404;你复制粘贴了一段Bootstrap导航栏代码,结果CSS全乱套,控制台报bootstrap.min.js:6 Uncaught TypeError: Cannot read property 'fn' of undefined;你写完Ajax提交表单,后端Servlet明明打了断点,但request.getParameter("title")永远是null——这不是玄学,是JavaWeb初学者在搭建个人博客时最真实的血泪现场。这个标题里的“servlet+bootstrap+ajax+jsp”不是技术堆砌,而是一条被验证过的最小可行路径:用原生Servlet处理请求与业务逻辑,用Bootstrap快速构建响应式UI,用Ajax实现无刷新交互(比如点赞、评论提交),用JSP作为视图层渲染动态内容(用户信息、文章列表、分页)。它不依赖Spring Boot自动装配,不引入Maven复杂依赖,适合课程设计验收、毕设开题演示、甚至作为JavaWeb工程师面试时可现场调试的“活体项目”。如果你需要一个能本地启动、数据库可换、代码结构清晰、错误有迹可循的落地方案,而不是一堆零散代码片段,这篇笔记就是为你写的。


2. 从零搭起项目骨架:目录结构、依赖配置与Servlet容器选型

2.1 目录结构必须遵循Servlet规范,否则JSP根本找不到Java类

JavaWeb项目不是把.java和.jsp文件随便扔进src或WebContent就能跑。IDEA新建Dynamic Web Project后,默认结构常埋雷:src下放Servlet类,但WEB-INF/classes没被正确映射;lib目录空着,连javax.servlet-api-4.0.1.jar都没加;更致命的是,web.xml放在WebContent/WEB-INF/下,但IDEA没把它识别为部署描述符。我一般会手动校验三层路径:

  • Java源码层:src/main/java/com/example/blog/(包名按实际改),所有Servlet、DAO、Bean类放这里;
  • Web资源层:src/main/webapp/(不是WebContent!这是Maven标准,即使非Maven项目也建议统一),里面放index.jsp、css/、js/、images/、WEB-INF/;
  • 部署描述符层:src/main/webapp/WEB-INF/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_4_0.xsd" version="4.0">——版本号错一位(比如写成3.1)会导致Tomcat 9+拒绝加载。

提示:不要用IDEA默认的“Add Framework Support”勾选Servlet,它可能生成过时的web.xml模板。直接删掉旧文件,用上面的XML头新建一个。

2.2 Tomcat 9.0.83 是当前最稳的Servlet容器选择,拒绝Tomcat 10+

课程设计环境追求稳定压倒一切。Tomcat 10+强制升级到Jakarta EE 9规范,所有包名从javax.*变成jakarta.*,这意味着你抄来的import javax.servlet.http.HttpServlet;会编译失败,而网上90%的JSP/Serlvet教程仍基于javax。实测Tomcat 9.0.83(2023年10月发布)对Servlet 4.0、JSP 2.3、EL 3.0支持完美,且与Bootstrap 5.3、jQuery 3.6兼容无压力。下载后解压,在IDEA中配置Server Runtime Environment时,务必指向apache-tomcat-9.0.83根目录,而非bin或lib子目录——这是新手最常翻车的一步。

2.3 依赖只加3个JAR:Servlet API、JDBC驱动、JSON解析器

这个博客不需要Hibernate或MyBatis。精简依赖是避免冲突的关键:

JAR文件版本作用获取方式
javax.servlet-api-4.0.1.jar4.0.1提供HttpServlet、HttpServletRequest等核心类Maven Repository下载,不要从Tomcatlib目录复制(会引发重复类加载)
mysql-connector-java-8.0.33.jar8.0.33MySQL 8+连接驱动,支持serverTimezone=UTC参数官网下载,注意8.0+版本需用com.mysql.cj.jdbc.Driver
json-simple-1.1.1.jar1.1.1Ajax返回JSON数据,比Jackson轻量,无反射依赖GitHub Release页下载,仅120KB

注意:这三个JAR必须放在src/main/webapp/WEB-INF/lib/下(不是src/lib或项目根目录),Tomcat启动时才会自动加入classloader。放错位置会导致ClassNotFoundException,且IDEA不会报编译错误——这是静默故障。

2.4 创建第一个Servlet:ArticleListServlet,验证请求路由是否打通

别急着写登录,先让首页能显示文章列表。创建com.example.blog.servlet.ArticleListServlet:

package com.example.blog.servlet; import com.example.blog.dao.ArticleDao; import com.example.blog.entity.Article; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/articles") public class ArticleListServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 查询数据库 List<Article> articles = new ArticleDao().findAll(); // 2. 存入request域,供JSP取值 request.setAttribute("articleList", articles); // 3. 转发到JSP页面(注意路径从WebRoot开始) request.getRequestDispatcher("/index.jsp").forward(request, response); } }

关键点说明:

  • @WebServlet("/articles")替代web.xml配置,更直观;但若用Tomcat 8以下,必须用web.xml方式;
  • request.getRequestDispatcher("/index.jsp")中的/表示Web应用根目录,即http://localhost:8080/your-project/index.jsp;
  • ArticleDao和Article实体类需提前写好,DAO里用Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/blog?serverTimezone=UTC", "root", "123456");——serverTimezone=UTC是MySQL 8+必加参数,否则报时区错误。

3. Bootstrap + JSP:如何让静态页面“活”起来,而不是CSS失效的摆设

3.1 Bootstrap 5.3 的CDN引入必须带integrity校验,否则本地开发会404

很多同学直接复制Bootstrap官网CDN:

<!-- 错误:没加integrity,离线开发时CDN不可达,页面白屏 --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

正确做法是加上SRI(Subresource Integrity)校验:

<!-- 正确:本地开发时CDN失效,自动回退到本地文件 --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6h4u4vhkR4j/o" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

提示:如果公司内网禁外网CDN,把bootstrap.min.css和bootstrap.bundle.min.js下载到src/main/webapp/css/和js/目录,改为<link href="css/bootstrap.min.css" rel="stylesheet">——但必须确保文件名和路径完全匹配,大小写敏感。

3.2 JSP动态渲染文章列表:用JSTL替代Scriptlet,告别<% %>灾难

别再用<% while(rs.next()) { %>这种写法。它让HTML和Java混杂,调试困难,且容易引发NullPointerException。改用JSTL(JavaServer Pages Standard Tag Library):

  1. 在web.xml中声明JSTL支持(Servlet 4.0必需):
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>
  1. 在index.jsp顶部引入JSTL核心标签库:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  1. 渲染文章列表(Bootstrap Card样式):
<div class="row"> <c:forEach items="${articleList}" var="article"> <div class="col-md-6 col-lg-4 mb-4"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title">${article.title}</h5> <p class="card-text text-muted small">${article.content.substring(0, 50)}...</p> <div class="d-flex justify-content-between align-items-center"> <span class="badge bg-primary">${article.author}</span> <small class="text-muted">${article.createTime}</small> </div> </div> <div class="card-footer"> <a href="article-detail.jsp?id=${article.id}" class="btn btn-sm btn-outline-primary">阅读全文</a> </div> </div> </div> </c:forEach> </div>

关键点说明:

  • ${articleList}来自Servlet的request.setAttribute("articleList", articles);
  • substring(0,50)在JSP中安全截取,避免StringIndexOutOfBoundsException(JSTL会自动处理null);
  • href="article-detail.jsp?id=${article.id}"生成带参数的URL,比JavaScript拼接更可靠。

3.3 Bootstrap表单与JSP联动:注册页如何把数据交给Servlet

一个典型的注册表单(register.jsp):

<form id="registerForm" action="RegisterServlet" method="post"> <div class="mb-3"> <label for="username" class="form-label">用户名</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="mb-3"> <label for="email" class="form-label">邮箱</label> <input type="email" class="form-control" id="email" name="email" required> </div> <div class="mb-3"> <label for="password" class="form-label">密码</label> <input type="password" class="form-control" id="password" name="password" required> </div> <button type="submit" class="btn btn-primary">注册</button> </form>

注意三个细节:

  • action="RegisterServlet":路径是相对于Web应用根目录,Servlet类名必须与@WebServlet("/RegisterServlet")一致;
  • name="username"等属性名,必须与Servlet中request.getParameter("username")的字符串完全相同(大小写敏感);
  • required属性由浏览器做前端校验,但后端Servlet必须再次校验,防止绕过。

4. Ajax请求设置编码格式:为什么中文总是乱码,以及如何一劳永逸解决

4.1 GET请求乱码:URL编码必须在前端JS中显式处理

当Ajax用GET传中文参数(如搜索关键词“JavaWeb”),后端request.getParameter("keyword")得到的是JavaWeb乱码。原因:浏览器默认用ISO-8859-1编码URL,而Tomcat用UTF-8解码。解决方案分两步:

前端JS(search.js):

function doSearch() { const keyword = document.getElementById('searchInput').value; // 关键:用encodeURIComponent强制UTF-8编码 const encodedKeyword = encodeURIComponent(keyword); fetch(`/search?keyword=${encodedKeyword}`) .then(response => response.json()) .then(data => renderResults(data)); }

后端Servlet(SearchServlet.java):

@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 关键:对GET参数单独设置编码 String keyword = request.getParameter("keyword"); if (keyword != null) { keyword = new String(keyword.getBytes("ISO-8859-1"), "UTF-8"); } // 后续业务逻辑... }

提示:new String(keyword.getBytes("ISO-8859-1"), "UTF-8")是Tomcat 9下GET乱码的标准解法。不要试图改server.xml的URIEncoding="UTF-8"——它只对路径有效,对查询参数无效。

4.2 POST请求乱码:Filter统一拦截比每个Servlet写setCharacterEncoding更可靠

为避免每个Servlet都写request.setCharacterEncoding("UTF-8"),创建EncodingFilter:

package com.example.blog.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 1. 设置请求编码(对POST表单有效) request.setCharacterEncoding("UTF-8"); // 2. 设置响应编码(对JSP输出有效) response.setContentType("text/html;charset=UTF-8"); // 3. 继续过滤链 chain.doFilter(request, response); } }

在web.xml中注册:

<filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.example.blog.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

4.3 Ajax返回JSON:用response.getWriter().write(jsonString)而非out.print()

很多同学用JSP内置对象out.print()输出JSON,结果页面上出现{"status":"success"}+一堆HTML乱码。正确做法是在Servlet中直接写响应体:

@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String title = request.getParameter("title"); String content = request.getParameter("content"); // ... 保存逻辑 // 关键:设置响应头,告诉浏览器这是JSON response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"status\":\"success\",\"message\":\"发布成功\"}"); }

注意:response.getWriter()和response.getOutputStream()不能共存,调用前者后不能再调用后者,否则抛IllegalStateException。


5. 避坑指南:那些让JavaWeb项目集体翻车的5个真实场景

5.1 现象:点击Bootstrap按钮没反应,控制台报Uncaught ReferenceError: $ is not defined

原因:jQuery未加载或加载顺序错误。Bootstrap 5+已移除jQuery依赖,但你的代码里还写了$("#myBtn").click(...)。
解决:要么降级到Bootstrap 4.6(仍需jQuery),要么改用原生JS:document.getElementById("myBtn").addEventListener("click", function(){...})。检查<script>标签顺序:jQuery必须在Bootstrap JS之前,且都在</body>前。

5.2 现象:JSP页面显示${article.title}原始字符串,而不是文章标题

原因:EL表达式被禁用。常见于web.xml中设置了<el-ignored>true</el-ignored>,或Tomcat版本与Servlet规范不匹配。
解决:确认web.xml中<el-ignored>false</el-ignored>;检查Tomcat版本(9.0+对应Servlet 4.0,支持EL3.0);删除JSP开头的<%@ page isELIgnored="true" %>。

5.3 现象:Ajax请求返回404 Not Found,但Servlet类明明存在

原因:URL路径错误。常见错误包括:action="servlet/RegisterServlet"(多写了servlet/前缀)、@WebServlet("/RegisterServlet")但访问http://localhost:8080/blog/RegisterServlet(漏了项目名blog)。
解决:在浏览器开发者工具Network标签页,看实际发出的请求URL是什么;对比@WebServlet注解路径与浏览器地址栏完整URL;用request.getContextPath()动态获取项目名。

5.4 现象:MySQL连接报错Access denied for user 'root'@'localhost',但命令行能登录

原因:MySQL 8+默认认证插件改为caching_sha2_password,而mysql-connector-java-8.0.33需显式指定useSSL=false&serverTimezone=UTC。
解决:JDBC URL改为jdbc:mysql://localhost:3306/blog?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true;或在MySQL中执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';。

5.5 现象:Tomcat启动后,修改JSP文件不生效,必须重启

原因:IDEA的On Update Action设置为No update,或Tomcat的reloadable="false"。
解决:在IDEA中,File → Settings → Build → Compiler → Build project automatically 勾选;Tomcat配置中,Deployment → Application context 设为/,并勾选Deploy at server startup;在conf/context.xml中,<Context reloadable="true">。


6. 让博客真正可用的3个进阶技巧:分页、图片上传、防XSS攻击

6.1 Bootstrap分页组件 + Servlet分页逻辑:一页10条,不查全表

前端index.jsp中插入Bootstrap分页HTML:

<nav aria-label="文章分页"> <ul class="pagination justify-content-center"> <c:if test="${currentPage > 1}"> <li class="page-item"><a class="page-link" href="?page=${currentPage-1}">上一页</a></li> </c:if> <c:forEach begin="1" end="${totalPages}" var="i"> <li class="page-item ${i == currentPage ? 'active' : ''}"> <a class="page-link" href="?page=${i}">${i}</a> </li> </c:forEach> <c:if test="${currentPage < totalPages}"> <li class="page-item"><a class="page-link" href="?page=${currentPage+1}">下一页</a></li> </c:if> </ul> </nav>

后端ArticleListServlet改造:

// 获取当前页码,默认第1页 int currentPage = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int pageSize = 10; int offset = (currentPage - 1) * pageSize; // 查询总记录数(用于计算总页数) int totalCount = new ArticleDao().countAll(); int totalPages = (int) Math.ceil((double) totalCount / pageSize); // 查询当前页数据 List<Article> articles = new ArticleDao().findPage(offset, pageSize); request.setAttribute("articleList", articles); request.setAttribute("currentPage", currentPage); request.setAttribute("totalPages", totalPages); request.getRequestDispatcher("/index.jsp").forward(request, response);

DAO层SQL示例(MySQL):

SELECT id, title, content, author, create_time FROM article ORDER BY create_time DESC LIMIT ?, ?

LIMIT ?, ?中的?对应offset和pageSize,防止SQL注入。

6.2 JSP图片上传:用<input type="file">+ ServletPartAPI,不依赖第三方库

upload.jsp表单:

<form action="UploadServlet" method="post" enctype="multipart/form-data"> <div class="mb-3"> <label for="articleImage" class="form-label">封面图片</label> <input type="file" class="form-control" id="articleImage" name="image" accept="image/*" required> </div> <button type="submit" class="btn btn-primary">上传</button> </form>

UploadServlet.java处理:

@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取上传文件Part Part part = request.getPart("image"); String fileName = Paths.get(part.getSubmittedFileName()).getFileName().toString(); // 生成唯一文件名,防止覆盖 String uniqueName = System.currentTimeMillis() + "_" + fileName; // 保存到服务器目录(绝对路径) String uploadPath = request.getServletContext().getRealPath("/uploads/"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); part.write(uploadPath + File.separator + uniqueName); // 返回JSON结果 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"status\":\"success\",\"url\":\"/uploads/" + uniqueName + "\"}"); }

注意:request.getPart("image")是Servlet 3.0+标准API,无需Apache Commons FileUpload;getRealPath("/uploads/")返回Web应用下的物理路径,确保/uploads/目录存在。

6.3 防XSS攻击:JSP输出时用<c:out value="${article.content}" escapeXml="true"/>

用户提交的文章内容可能含<script>alert('xss')</script>。若直接${article.content}输出,脚本会执行。正确做法:

<div class="card-text"> <c:out value="${article.content}" escapeXml="true"/> </div>

escapeXml="true"将<转为&lt;,>转为&gt;,彻底阻断HTML标签解析。这是JSTL提供的安全输出方式,比自己写StringEscapeUtils.escapeHtml4()更轻量。

最后说句实在话:这个博客项目我带过三届学生,从建项目到答辩,平均耗时12小时。最大的教训是——别在没跑通Servlet跳转前就折腾Bootstrap样式,也别在Ajax没返回JSON前就写前端渲染逻辑。把/articles能打开、/RegisterServlet能接收参数、/UploadServlet能存文件这三件事做扎实,后面全是填空题。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 3:19:02

Cadence Design Entry HDL从入门到实战:原理图设计流程与避坑指南

做硬件设计这些年&#xff0c;Cadence 这套工具始终是绕不开的核心。很多人提到 Cadence&#xff0c;第一反应是 OrCAD Capture 或者 Allegro PCB Editor&#xff0c;但如果你进过通信、服务器、工控这类复杂板卡公司&#xff0c;大概率会遇到另一个名字&#xff1a;Design Ent…

作者头像 李华
网站建设 2026/10/8 3:18:06

亚马逊新品榜API采集指南:用Python搭建选品数据筛选与评分模型

1. 为什么说新品榜藏着选品机会做亚马逊选品的朋友应该都有这种体会&#xff1a;BSR大类榜和飙升榜看得再多&#xff0c;轮到自己上架时&#xff0c;总觉得慢了半拍。原因很简单&#xff0c;大类榜反映的是已经被市场验证过的成熟产品&#xff0c;等你看到数据再入场&#xff0…

作者头像 李华
网站建设 2026/10/8 3:17:49

C++静态检测实战:从cppcheck到clang-tidy的CI集成与误报治理

1. 静态检测这件事&#xff0c;我为什么劝你先安排上接触C项目的人&#xff0c;多少都有过这种体验&#xff1a;编译通过、功能跑通、测试也绿&#xff0c;代码合并上线之后&#xff0c;生产环境出了诡异问题。查了半天发现是某个分支里数组越界、某个对象被提前释放、某个类型…

作者头像 李华
网站建设 2026/10/8 3:17:11

Python股票数据分析系统:从数据采集到可视化全流程实战

1. 内容整体设计与思路拆解1.1 这个系统解决什么问题说句实话&#xff0c;股票数据分析这事儿&#xff0c;很多人一上来就想着搞预测模型、机器学习&#xff0c;结果数据都没整明白&#xff0c;模型跑出来全是噪声&#xff0c;最后连自己都骗了。我做这个Python股票数据分析系统…

作者头像 李华
网站建设 2026/10/8 3:17:10

放疗优化中的伴随灵敏度分析:原理、Matlab实现与工程实践

放疗计划里有个我必须认真对待的问题&#xff1a;肿瘤不是一块儿静物。它一边在增殖、扩散&#xff0c;一边又对辐射产生不同程度的反应&#xff0c;而你的剂量方案却希望以不变应万变。现实中&#xff0c;肿瘤生长模型里的增殖率、扩散系数这些参数全是估计值&#xff0c;伴随…

作者头像 李华