简介:基于JavaWeb的文章管理系统源码与数据库,面向计算机相关专业学生及JavaWeb初学者,可完整实现用户与管理员双角色登录、用户发布新文章、文章详情查看、文章修改、删除与恢复等功能,适合作为课程设计、大作业或入门实战练习项目。资源包共396个文件,约7.32MB,以java源码、jsp页面、js脚本、css样式及class编译文件为主,另含sql数据库脚本与jar依赖,导入数据库即可运行,目录结构清晰便于对照学习。已有154人浏览学习。借助源码中文章新增、查询、修改、删除、恢复及登录校验等核心类的实现,可掌握JavaWeb项目从数据库设计、后端逻辑处理到前端页面渲染的完整流程。同时可借鉴登录鉴权、分页查询等模块写法,为独立开发内容管理系统提供可复用模板。
1. 这个zip里装的JavaWeb文章管理系统,先看它解决了什么
从zip解压出来的是“源码+数据库”的完整JavaWeb项目:用户和管理员登录、用户发布新文章、文章详情查看、文章删除与恢复。这套功能几乎是所有课设题目里出现频率最高的一套组合,也是你现在面试或练手时常被问到的增删改查闭环。你真正想知道的是:它能不能跑起来,怎么改,删了怎么找回。
我一般是先打开WebContent/WEB-INF看目录结构再决定运行方式。这个项目的技术路线非常常规,Servlet + JSP + JDBC + MySQL,登录用Session,删除用逻辑删除is_deleted字段,文章表加用户表就两三百行SQL,数据表一导、Tomcat一放就能跑。和那些动辄引入Spring全家桶的工程相比,它读起来更像你刚学完Servlet后想自己做出来的成品。
这个东西适合两类人:做课设要选型的学生,以及想快速拿一套完整JavaWeb代码去改成个人博客或内部公告系统的开发者。它的边界也很清楚:没有Spring、没有集群、没有富文本编辑器,属于小而完整的一类。真正的价值在于读得懂、改得动、能讲清楚,这也正是我下面要带你过一遍的东西。
2. 表结构与逻辑删除:文章管理系统的基础
把zip里的SQL脚本打开,最先看到的通常是两张表:user和article。绝大多数功能跑不跑得通,在这两张表的结构里就决定了一半。我以前做课设时急着看代码,后来发现表设计才是应该先读的东西,一个is_deleted字段直接决定了后面的删除与恢复要怎么写SQL。
2.1 用户表和文章表:字段怎么定才不返工
常见做法是两张表对应两个核心实体,我先把建表SQL贴出来:
CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(64) NOT NULL, `role` VARCHAR(20) DEFAULT 'user', `status` INT DEFAULT 1, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `article` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `content` TEXT, `author_id` INT NOT NULL, `view_count` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `is_deleted` INT DEFAULT 0, `delete_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_author` (`author_id`), KEY `idx_deleted` (`is_deleted`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里把密码列固定为64字符,是为了预留MD5加盐或SHA一类哈希值的存储空间。文章表用author_id而不是直接存author_name,因为用户改名后文章归属不会被带乱,查询列表时只需要JOIN一次user表拿用户名即可。字段设计的逻辑是:role区分用户和管理员,status用于禁用账号而不是删行,这是双角色登录的第一道关卡;view_count做浏览计数,详情页每次访问+1,这个字段在验收“文章详情查看”时会用到。
is_deleted和delete_time是这套系统的核心,没有这两个字段,“删除与恢复”只能靠物理删行来做。而物理删除一旦执行,数据就永久消失了,恢复功能也就无从谈起。所以建表阶段一定要看到is_deleted默认值为0,delete_time默认允许为NULL,这是逻辑删除的基本形态。
2.2 逻辑删除 vs 物理删除:删除与恢复的开关
文章删除与恢复统一用逻辑删除是最稳的做法:删除不等于让记录消失,而是给记录打标记。用户点“删除”,实际执行的是UPDATE而不是DELETE:
-- 删除:标记为已删除 UPDATE article SET is_deleted = 1, delete_time = NOW() WHERE id = ?; -- 恢复:取消标记,清除删除时间 UPDATE article SET is_deleted = 0, delete_time = NULL WHERE id = ?;列表默认查询时要带上is_deleted = 0条件,其余业务代码完全不用动。这个方案的好处有两点:第一,恢复功能不需要额外备份表,一行UPDATE就能把数据拉回来;第二,误删有后悔药,对做课设或内部小系统的人来说,这一点在演示和自测时特别有用。代价是你在项目里所有查询文章的地方都要记得过滤,漏一个就会出现“删除后刷新文章又出现”的翻车现场。
有的项目会设计专门的trash表来存放已删除记录,删除时先INSERT进trash再DELETE原记录。这种方案审计上更清晰,但代码量和状态同步成本直接翻倍,对文章管理系统来说完全不划算。一个is_deleted字段加delete_time就能覆盖“删除与恢复”的验收要求,后面想加定时物理清理也只是一条DELETE语句的事。我的选择理由很直接:按最小方案实现,把复杂度留给分页和权限这些真正影响体验的部分。
2.3 双角色权限与登录会话
双角色的权限控制不复杂但容易漏。user表的role取值是user或admin,控制层用一个Filter去判断请求路径。常见做法是:/admin/*路径走管理员过滤器,检查session里的role;发布文章走登录过滤器,检查userId;文章详情和列表页是公开的,不需要登录。
我见过有同学把登录判断直接写在JSP页面里,每个页面重复判断一次,加第二个功能后维护成本就失控了。正确的做法是把登录信息放进会话,登录成功后存三个值:userId、username、role。过滤器只需要判断userId是否为null,为null就重定向到login.jsp。这个设计也顺便解答了一个常见疑问:用户和管理员不需要两张表,一个user表加role字段就够,登录统一走同一个LoginServlet,密码校验通过后根据role决定跳转后台还是主页。
注意过滤器的执行顺序按web.xml里filter-mapping的声明顺序排列。登录过滤器要排在最前,编码过滤器其次,管理员权限过滤器放最后。顺序写反会出现编码过滤器还没执行就被登录过滤器拦走,导致会话里的中文参数读取异常。这个问题不太容易发现,遇到了先按这个顺序调。
3. 登录会话与文章发布:从点击登录到文章落库的全链路
下面把四个核心功能串成一条线:登录会话、发布文章、详情查看、删除恢复。每个环节都会给出可以直接复制的代码,以及我在实际部署时调过的参数。
3.1 登录接口最小实现:MD5加盐与Session写入
先看LoginServlet的关键代码:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 编码必须放在第一次 getParameter 之前,否则中文必然乱码 req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.findByUsername(username); // 密码统一走 MD5 + 固定盐,盐值放在常量类里 String md5Pwd = MD5Util.md5(password + SALT); if (user == null || !user.getPassword().equals(md5Pwd)) { req.setAttribute("msg", "用户名或密码不对"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } if (user.getStatus() != 1) { req.setAttribute("msg", "账号已被禁用"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 登录成功,把用户标识写进 Session req.getSession().setAttribute("userId", user.getId()); req.getSession().setAttribute("username", user.getUsername()); req.getSession().setAttribute("role", user.getRole()); resp.sendRedirect("index"); }逻辑说明:先按username查用户,用户不存在和密码错误统一提示“用户名或密码不对”,这样不会暴露账号是否存在。密码校验用的是MD5加固定盐,这是课设阶段最安全且易写的方案,比明文存数据库高一个量级;如果你做正式项目,把这句话改成BCrypt即可,接口思路不变。
参数说明:login.jsp的form表单提交方式必须为post,因为只有post方式下setCharacterEncoding才能正确解码中文;session默认30分钟无操作失效,这个值课设一般不改。登录成功用sendRedirect而不是forward,是为了防止用户按F5刷新时重复提交登录表单。
3.2 发布新文章:中文编码、XSS过滤与insert落库
发布文章的核心逻辑在ArticleServlet的doPost:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); HttpSession session = req.getSession(); // 未登录用户直接回登录页,不执行后续任何查询 Object userId = session.getAttribute("userId"); if (userId == null) { resp.sendRedirect("login.jsp"); return; } String title = req.getParameter("title").trim(); String content = req.getParameter("content"); Article article = new Article(); // 把标题与内容里的 HTML 尖括号转义,防止脚本注入 article.setTitle(HtmlUtil.clean(title)); article.setContent(HtmlUtil.clean(content)); article.setAuthorId(Integer.parseInt(userId.toString())); int rows = articleDao.insert(article); resp.sendRedirect("index"); }逻辑说明:第一行setCharacterEncoding必须出现在第一次读取请求参数之前,否则中文字符到这里已经损坏,后面再设置也没有意义。先校验登录再取参数,避免未登录的人直接往文章表里写数据。HtmlUtil.clean是一个工具方法,内部用replace把<、>、&替换成HTML实体,能挡住大部分常见的脚本提交。发布成功后统一跳回列表页,新文章就会出现在首页。
参数说明:content字段在表里是TEXT类型,默认上限是64KB,中文按字符数计算,一篇几千字的文章完全够用;如果以后要写长文,把字段改成MEDIUMTEXT即可。这里有一个很多新手会踩的坑:在setCharacterEncoding之前调用getParameter,那这行代码就白写了,一定要把它放在Servlet最顶部。
3.3 文章详情查看:主键查询与浏览计数
详情查看用doGet就能实现:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idParam = req.getParameter("id"); if (idParam == null || idParam.isEmpty()) { resp.sendError(400); return; } int id = Integer.parseInt(idParam); Article article = articleDao.findById(id); // 已删除的文章直接返回 404,防止通过路径直接访问 if (article == null || article.getDeleted() == 1) { resp.sendError(404); return; } // 浏览量 +1,刷新一次就加一次,课设可接受 articleDao.incrementViewCount(id); req.setAttribute("article", article); req.getRequestDispatcher("/article_detail.jsp").forward(req, resp); }逻辑说明:id参数先做非空校验,避免Integer.parseInt收到null时抛空指针。findById内部SQL是SELECT * FROM article WHERE id = ? AND is_deleted = 0,这样被删除的文章在详情页直接表现为404,用户感知上就是“真删掉了”。incrementViewCount执行UPDATE article SET view_count = view_count + 1,如果刷新页面每次都加一次,这是可接受行为;想要精确去重可以改成按Session判断。
参数说明:resp.sendError(400)会返回Tomcat默认的错误页,比在页面上alert“参数错误”更省事。Integer.parseInt在实战中最好用try-catch包住,传了非数字时走统一异常处理,否则Tomcat默认报错页对用户不够友好。
3.4 删除与恢复:逻辑置位、回收站与彻底清除
后台一共三个接口,分别对应删除、恢复、彻底删除:
// 删除:把 is_deleted 置为 1,保留数据 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id = Integer.parseInt(req.getParameter("id")); articleDao.logicDelete(id); resp.sendRedirect("admin_article_list"); } // 恢复:把 is_deleted 置回 0,清除 delete_time protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id = Integer.parseInt(req.getParameter("id")); articleDao.restore(id); resp.sendRedirect("admin_trash"); } // 彻底删除:只允许角色为 admin 的人执行,执行后不可恢复 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (!"admin".equals(req.getSession().getAttribute("role"))) { throw new ServletException("无权限"); } articleDao.physicalDelete(id); resp.sendRedirect("admin_trash"); }逻辑说明:删除和恢复都是逻辑置位操作,成功后分别跳转到管理列表和回收站列表。彻底删除前做了一次角色校验,因为一旦DELETE FROM执行完就没有后悔药。三种接口共用同一张article表,区别只在条件不同,核心SQL如下:
-- 逻辑删除 UPDATE article SET is_deleted = 1, delete_time = NOW() WHERE id = ?; -- 恢复 UPDATE article SET is_deleted = 0, delete_time = NULL WHERE id = ?; -- 彻底删除 DELETE FROM article WHERE id = ?; -- 回收站列表 SELECT * FROM article WHERE is_deleted = 1 ORDER BY delete_time DESC;参数说明:管理列表只查is_deleted=0的数据,回收站只查is_deleted=1的数据,两套列表的数据是同一张表。前端页面上“移入回收站”和“彻底删除”要分成两个按钮,彻底删除按钮加一次二次确认,这能避免演示时手滑把数据清掉。
4. 部署与运行:从zip到浏览器能访问的完整步骤
代码看懂了,更重要的是把项目跑起来。很多读者卡在“源码+数据库”这一步,不是代码有问题,而是导入和配置顺序不对。按照下面三步走,基本能十分钟内跑通。
4.1 数据库初始化:用SQL脚本一次建库导数据
打开zip里的SQL脚本,命令行方式导入最稳妥:
mysql -u root -p CREATE DATABASE IF NOT EXISTS article_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE article_db; source /your/path/article_db.sql;逻辑说明:先建库,指定utf8mb4字符集,再source导入表结构和初始数据。常见错误是忘记指定字符集,导致Java端配置了UTF-8,数据库表却还是latin1,中文乱码问题就很难排查。source路径如果含中文或空格,需要用引号包住整个路径,否则mysql客户端会在解析路径时报错。
参数说明:MySQL 5.7和MySQL 8都能执行这套SQL,但连接MySQL 8时驱动名要换。如果源码里给的是com.mysql.jdbc.Driver,在8.x下会直接报ClassNotFoundException,这个细节放在第5章展开。导入完成后执行SHOW TABLES确认出现user和article两张表,再查一下user表里有没有预置的管理员账号,有就说明初始数据导入成功。
4.2 Tomcat与JDK版本:课设最稳的组合
我一般推荐JDK 8搭配Tomcat 8.5。这个组合对传统JavaWeb最友好,Servlet用的是javax.servlet命名空间,不需要改任何代码。
java -version # 确认是 jdk1.8.0_ 开头的版本 cd $TOMCAT_HOME/webapps ls # 把解压后的项目文件夹整个放进来逻辑说明:把解压后的项目文件夹放进webapps目录,启动Tomcat后就能通过端口加项目名访问。如果文件夹叫article,访问地址就是http://localhost:8080/article/index。zip解压后如果已经是标准的WebContent目录结构,不需要重新打war包,直接把整个文件夹丢进去即可。这一步要检查项目lib目录里有没有mysql驱动jar,很多启动报错就出在驱动没被shipping进去。
参数说明:Tomcat 10的用户要特别注意,Tomcat 10已经把API从javax迁移到jakarta命名空间,旧课设代码直接部署会报NoClassDefFoundError,除非源码里import的是jakarta.servlet,否则别拿Tomcat 10跑课设。拿不准就看源码第一行import或web.xml里的命名空间声明。
如果8080端口被占用,启动时会报Address already in use。处理办法是先找到占用进程:Linux下用lsof -i:8080,Windows下用netstat -ano | findstr 8080。也可以直接改server.xml里的端口号。传统JavaWeb项目的端口和上下文路径都是可配置的,这一点比Spring Boot那套更透明,能让你更清楚理解每次访问背后走的链路。
4.3 连接池配置:db.properties的参数与典型值
传统JavaWeb项目通常用db.properties管理数据库连接:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/article_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 jdbc.initialSize=5 jdbc.maxActive=20 jdbc.maxIdle=10 jdbc.minIdle=5逻辑说明:driver和url必须匹配。MySQL 5.x用com.mysql.jdbc.Driver,MySQL 8.0以上用com.mysql.cj.jdbc.Driver,url一定要带serverTimezone=Asia/Shanghai,否则连接时会报时区错误。useSSL=false是本地开发时避免SSL握手警告。initialSize是启动时创建的连接数,maxActive是并发峰值上限,课设项目maxActive=20就足够。
参数说明:如果项目用的是Druid连接池,db.properties里的键名要改成druid.initialSize、druid.maxActive这类前缀,不能直接写jdbc.initialSize。去源码里看连接池工厂类读取的key前缀再配置,这是我见过很多次的部署翻车点。
部署完成后打开浏览器访问http://localhost:8080/项目名/login.jsp,能渲染出登录页说明Servlet映射没有问题。此时数据库连接还没被动用,真正的连通性要点击登录按钮时才验证。我习惯按这个顺序确认:先登录,再发一篇文章,最后检查后台列表。这样每走一步,出问题时定位范围非常小。
5. 避坑记录:乱码、驱动、路径与删除恢复的整改记录
这些坑我都走过,每条按“现象、原因、解决”三连接着往下说。
5.1 页面中文全是问号
现象:首页能访问,但登录后发布的中文标题显示成“???”或乱码。原因:三处编码不一致。请求编码、响应编码、数据库表字符集只要有一个不对,中文链路就断。很多项目只设置了request.setCharacterEncoding("UTF-8"),但JSP页面没有指定contentType,或者数据库建表用了latin1。
解决:JSP页面顶部统一加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;Tomcat的server.xml里给Connector加URIEncoding="UTF-8";建表统一用utf8mb4;连接串带characterEncoding=utf8。四步全做到,中文乱码基本不会再出现。
5.2 登录报ClassNotFoundException
现象:登录时提示ClassNotFoundException: com.mysql.jdbc.Driver,或者卡很久之后报Access denied for user。原因:项目lib里放的是旧的mysql-connector-java 5.x,但你本机是MySQL 8;另一种情况是db.properties里的账号密码和本机MySQL不一致。
解决:把驱动jar换成mysql-connector-java 8.0.x,db.properties里的driver改成com.mysql.cj.jdbc.Driver,url加上serverTimezone=Asia/Shanghai。如果报Access denied,直接改成你本机真实的username和password,不要照抄网上的123456。
5.3 删除后刷新又出现
现象:点击删除提示成功,刷新列表文章还在。原因:删除接口执行了UPDATE把is_deleted置1,但列表查询SQL是SELECT * FROM article,没有拼is_deleted=0条件,逻辑删除根本没有产生效果。
解决:全项目所有查询文章的SQL统一加WHERE is_deleted = 0,详情查询也要加。这是逻辑删除方案唯一的隐藏成本,漏一个就翻车。我习惯把is_deleted=0条件直接写进DAO的公共查询片段里,让所有列表和详情复用,从源头上杜绝漏加。
5.4 恢复按钮404
现象:在回收站点“恢复”,URL变成/restore?id=3,结果404。原因:JSP里写的链接是绝对路径/restore,而项目部署路径不是根路径,请求落到了Tomcat根上下文之外。如果项目部署在http://localhost:8080/article,那不带上下文路径的链接必然访问不到。
解决:所有链接统一用${pageContext.request.contextPath}拼路径,例如href="${pageContext.request.contextPath}/article/restore?id=${a.id}"。同时检查Filter的放行规则,如果过滤器拦截放行规则太糙,拦截了login.jsp或静态CSS,就会出现“登录成功又弹回登录页”的怪现象。
5.5 Session过期后表单被拦截
现象:在编辑页面停留超过30分钟,点发布直接被Filter拦回登录页,写的文章内容全部丢失。原因:Session默认30分钟无操作失效,登录过滤器只认userId是否为空,过期后页面还停留在编辑状态,一提交就被拦。
解决:短平快做法是在web.xml里把session超时改到60分钟;更细的做法是发布页用localStorage定时保存草稿,提交成功后清除;正式项目可以在登录拦截器里带redirectURL参数,登录成功后自动回跳。课设阶段把超时调大加草稿保存,体验就已经很好了。
6. 进阶验证:把课设改成一个能解释清楚的小作品
功能跑通之后,建议再做一遍“验收加小改”,这个动作能让你在演示时明显比别人讲得清楚。
6.1 最小验收清单:按清单快速验证每个功能
| 功能 | 操作 | 预期结果 |
|---|---|---|
| 用户登录 | 输入普通用户账号 | 跳转到普通用户首页 |
| 管理员登录 | 输入管理员账号 | 跳转到管理后台 |
| 发布文章 | 填写标题与内容提交 | 列表出现新文章,详情可访问 |
| 查看详情 | 点击文章标题 | 正常渲染内容,浏览量加1 |
| 删除文章 | 列表页点删除 | 文章从列表消失,进入回收站 |
| 恢复文章 | 回收站点恢复 | 文章重新出现在列表 |
6.2 三个值得顺手改的小功能:分页、日志、防注入
第一个是列表分页。现在的列表是全量查询,文章一多就难找。改动很小:加pageNo和pageSize两个参数,SQL加LIMIT offset, pageSize,前端渲染上一页下一页按钮。第二个是操作日志。每次删除、恢复、彻底删除都往log表写一条记录,包含操作用户和文章标题,这是演示时的加分项,也能让你之后排查误删时有据可查。第三个是SQL全面改PreparedStatement,杜绝字符串拼接传参数,这是安全底线不是性能优化。
我有一个习惯:拿到这类zip源码,先看SQL脚本和db.properties,再先跑通登录,最后才动业务代码。把乱码、驱动、路径这三个问题处理掉,剩下的大多是逻辑问题,反而好修。如果你准备把这个文章管理系统交作业或挂到个人网站上,把上面提到的分页和日志一起补上,完整度立刻上一个台阶。希望帮到你。
本文还有配套的精品资源,点击获取