
简介基于JavaWeb的文章管理系统源码与数据库打包在一起定位于课程设计/新闻发布类实战项目适合计算机专业学生用于大作业、课设或毕业设计演示。系统区分用户与管理员登录涵盖发布新文章、详情查看、修改、删除与恢复等核心流程并由SQL脚本完成数据库初始化方便二次开发。压缩包共396个文件、约7.32MB主要包含JSP/Servlet源码java/jsp/class、数据库脚本、jar依赖以及js/css/png/gif等前端资源整体为典型JavaWeb工程结构导入IDE即可运行调试。已有154人学习下载源码中的NewsRealeseDao、QueryOneNews、UpdateOneNews、DeleteOneNews、ResumeNews等关键类有助于理解登录鉴权、增删改查与逻辑恢复的实现思路适合边练习边梳理完整开发流程。1. 能直接答辩的JavaWeb文章管理系统登录、发布、回收站一条链路都是现成的做课设最怕的不是功能写不出来而是写完之后跑不起来。这套基于JavaWeb的文章管理系统新闻管理系统好就好在用户和管理员两套登录入口、用户发布新文章、文章详情查看、修改、删除进回收站、再从回收站恢复全程都是现成的闭环。类名起得直白到不用翻文档——InsertOneNews管插入、QueryOneNews管查询、ResumeNews管恢复拿来当课程设计交差完全够格想二次开发或者做毕设起始骨架也合适。适合正在找JavaWeb课程设项目参考、想快速看懂ServletJSPJDBC三层写法的同学也适合答辩前想找一份「能跑、敢演示」的完整源码来对照检查的开发者。2. 技术栈与项目结构Servlet JSP JDBC 的课设黄金三角2.1 技术选型为什么这套组合到现在还是课设主流这个项目没有用Spring、没有用MyBatis核心就是纯JavaWeb三件套。主语言是Java 8或更高版本Web容器用的Tomcat8或9都行数据库MySQL版本5.7或8.0均能跑。Servlet版本跟着Tomcat走用的是Servlet 3.0以上的API所以类上可以直接写WebServlet注解不用在web.xml里堆一堆映射配置。有人会问现在企业里都在用Spring Boot学这套老掉牙的东西有什么用。我的看法是课设的核心目标不是炫技而是让你亲手把「浏览器发请求 → Servlet接请求 → JDBC查数据库 → 页面渲染返回」这条完整路径走通。这个链路你亲手写过一遍后面再看Spring MVC就非常快因为Spring MVC本质上就是把Servlet这一层包了一层壳核心思想没变。另外这种项目的运行成本极低。不需要Maven仓库拉一堆依赖IDE里配好Tomcat直接Run就能起来对机器配置和网络环境要求都不高。拷到U盘里带到答辩教室临时给老师演示也不容易翻车。2.2 源码目录与分层看一眼类名就能定位功能从资源里那组类名能直接看出分层思路我整理了一张对照表类名所属模块职责定位checkLogin_user登录模块校验用户名密码、写Session、登录跳转Authorize权限模块过滤器拦截未登录用户访问受保护页面QueryOneNews_user用户端按文章ID查详情给普通用户浏览文章QueryOneNews查询模块按文章ID查单条给编辑和管理场景用InsertOneNews发布模块接收表单参数插入新文章UpdateOneNews编辑模块按ID更新文章标题和内容DeleteOneNews删除模块逻辑删除写is_deleted标记进回收站ResumeNews恢复模块把回收站里的文章还原checkNews校验模块文章参数校验或存在性检查NewsRealeseDao数据层负责所有SQL操作的DAO注意看这组类的命名存在两对呼应关系DeleteOneNews和ResumeNews是一对对应回收站功能的「删」与「恢复」QueryOneNews和QueryOneNews_user也是一对分别服务管理端和普通用户端。这种命名方式朴素但对课设有个实打实的好处——答辩时老师问某个功能在哪你直接报类名就行不用翻半天代码。代码上常见做法是这样一个包结构src/ ├─ com/xxx/bean/ # 实体类News、User ├─ com/xxx/dao/ # NewsRealeseDao 等数据操作类 ├─ com/xxx/servlet/ # InsertOneNews、DeleteOneNews 等 ├─ com/xxx/filter/ # Authorize └─ com/xxx/util/ # DBUtil、StringUtil 等工具类有些课设版本会省掉Service层让Servlet直接调DAO。对课程设计来说这样可以接受但想二次开发的话建议在中间补一层Service后面第5章会细说。2.3 数据库设计核心一张文章表撑起回收站逻辑这类系统的数据库结构一般比较简单核心就两张表用户表、文章表。有的版本会再加一张文章分类表。用户表典型的建表语句长这样CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT DEFAULT 1 COMMENT 1-普通用户 2-管理员 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;文章表是最能体现这个系统业务设计的一张表CREATE TABLE t_news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, author_id INT NOT NULL COMMENT 关联t_user.id, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1-已发布 2-草稿, is_deleted TINYINT DEFAULT 0 COMMENT 0-正常 1-回收站 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里最关键的字段是is_deleted。DeleteOneNews执行的效果不是DELETE FROM t_news WHERE id?而是UPDATE t_news SET is_deleted1 WHERE id?。这样一来正常列表查询只要在SQL后面加WHERE is_deleted0就永远看不到回收站数据回收站页面用WHERE is_deleted1查出来再配合UPDATE ... SET is_deleted0恢复就完成了「删除 → 回收站 → 恢复」的完整闭环。从这张表还能看出一层隐藏关系author_id是外键指向t_user.id。如果课设要求显示文章作者名通常通过两表JOIN查询或者先查文章再查用户的方式实现。我见过不少版本直接在文章表里冗余一个author_name字段图省事虽然能跑但用户改名时会造成数据不一致不建议这么干。3. 核心功能代码拆解从点击按钮到数据落库的完整链路3.1 登录与鉴权checkLogin_user Authorize 的拦截组合登录是整个系统的入口checkLogin_user负责处理登录请求。常见实现是这样一个ServletWebServlet(/login) public class CheckLoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); NewsRealeseDao dao new NewsRealeseDao(); User user dao.findUserByLogin(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); // 管理员跳后台普通用户跳首页 if (user.getRole() 2) { response.sendRedirect(adminIndex.jsp); } else { response.sendRedirect(index.jsp); } } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这段代码的逻辑很直接先拿到表单里的用户名和密码调DAO按用户名密码查用户对象。查到了就写进Session查不到就回登录页并在request里塞一个错误提示。关键点在于Session的写入——后面所有权限判断都依赖loginUser这个属性是否存在。登录成功之后的请求保护交给Authorize这个过滤器处理WebFilter(/*) public class Authorize implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); // 静态资源和登录页放行 if (uri.endsWith(login.jsp) || uri.endsWith(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.jpg) || uri.endsWith(.png)) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(login.jsp); return; } chain.doFilter(req, resp); } }过滤器的处理顺序很关键先放行静态资源和登录相关的路径再判断Session里有没有用户。request.getSession(false)和getSession()有区别前者在Session不存在时返回null不会强制创建新Session避免每次静态资源请求都产生一个多余的服务端Session对象。3.2 发布新文章InsertOneNews 到 NewsRealeseDao 的写入流程用户登录后在发布页填标题和内容点击提交请求打到InsertOneNews这个Servlet上。典型实现长这样WebServlet(/addNews) public class InsertOneNews extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(login.jsp); return; } String title request.getParameter(title); String content request.getParameter(content); if (title null || title.trim().isEmpty()) { request.setAttribute(error, 标题不能为空); request.getRequestDispatcher(addNews.jsp).forward(request, response); return; } News news new News(); news.setTitle(title); news.setContent(content); news.setAuthorId(user.getId()); news.setStatus(1); NewsRealeseDao dao new NewsRealeseDao(); int rows dao.insert(news); if (rows 0) { response.sendRedirect(index.jsp); } else { request.setAttribute(error, 发布失败请重试); request.getRequestDispatcher(addNews.jsp).forward(request, response); } } }注意两个细节。第一发布前先从Session里取loginUser对象拿到当前登录用户的ID作为作者ID这比让前端表单传authorId可靠得多——否则用户可以伪造请求改作者。第二标题做了非空校验但content没有强校验实际项目里应该根据业务补字数限制或敏感词过滤。配合的DAO插入方法public int insert(News news) { String sql INSERT INTO t_news(title, content, author_id, status) VALUES(?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, news.getTitle()); ps.setString(2, news.getContent()); ps.setInt(3, news.getAuthorId()); ps.setInt(4, news.getStatus()); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } }这里用的是PreparedStatement占位符而不是拼接SQL字符串是必须养成的习惯。好处有两个一是防止单引号、反斜杠等特殊字符把SQL搞坏二是防SQL注入。参数说明setString(1, ...)对应title列setString(2, ...)对应content列setInt(3, ...)对应author_idsetInt(4, ...)对应status。位置必须和SQL里的?顺序一一对应。3.3 文章详情与修改QueryOneNews 和 UpdateOneNews 的配合用户从列表页点某篇文章标题跳到详情页。这个动作由QueryOneNews_user或QueryOneNews处理两者逻辑基本一样差别在于查询后跳转的页面不同。核心代码是protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int id Integer.parseInt(request.getParameter(id)); NewsRealeseDao dao new NewsRealeseDao(); News news dao.findById(id); if (news null) { response.sendRedirect(index.jsp); return; } request.setAttribute(news, news); request.getRequestDispatcher(newsDetail.jsp).forward(request, response); }DAO里的findById对应SQLpublic News findById(int id) { String sql SELECT id, title, content, author_id, status, create_time FROM t_news WHERE id? AND is_deleted0; // try-with-resources 执行查询把 ResultSet 映射为 News 对象 }注意这里的SQL末尾带is_deleted0保证即使有人拿着回收站文章的ID直接访问详情地址也看不到内容。这是很容易漏掉的一个过滤条件后面第4章会再展开。修改文章时UpdateOneNews的常规流程是先按ID查出原文章再接收表单提交的新标题和新内容更新数据库。这里有个课设里常见的权限漏洞——如果页面里放了一个隐藏的id字段用户可以通过修改HTML源码把这个ID改成别人的文章ID从而改掉别人的内容。常规做法是在Servlet里加一层校验User loginUser (User) session.getAttribute(loginUser); News oldNews dao.findById(id); if (loginUser.getRole() ! 2 oldNews.getAuthorId() ! loginUser.getId()) { request.setAttribute(error, 只能修改自己发布的文章); request.getRequestDispatcher(error.jsp).forward(request, response); return; }管理员角色值为2时放行所有文章的修改普通用户只能改author_id等于自己ID的文章。这行判断在很多课设版本里是缺失的补上之后答辩会稳很多。3.4 删除与恢复软删除标记和ResumeNews翻转状态删除功能的实现是这个项目最值得看的部分。DeleteOneNews并不是物理删除数据而是把is_deleted字段置为1WebServlet(/deleteNews) public class DeleteOneNews extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int id Integer.parseInt(request.getParameter(id)); NewsRealeseDao dao new NewsRealeseDao(); int rows dao.updateDeleted(id, 1); // 根据操作者身份决定回到哪个页面 response.sendRedirect(trash.jsp); } }恢复功能ResumeNews做的事情正好相反WebServlet(/resumeNews) public class ResumeNews extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int id Integer.parseInt(request.getParameter(id)); NewsRealeseDao dao new NewsRealeseDao(); int rows dao.updateDeleted(id, 0); if (rows 0) { response.sendRedirect(QueryOneNews_user?actiontrashList); } else { response.sendRedirect(error.jsp); } } }DAO里两个方法共用一个updateDeletedpublic int updateDeleted(int id, int deleted) { String sql UPDATE t_news SET is_deleted? WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, deleted); ps.setInt(2, id); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } }参数说明deleted传1表示删除进回收站传0表示恢复正常。因为只更新一个标志位不涉及数据搬迁所以恢复操作执行得很快。这样设计的最大价值是给了用户一次「后悔药」——误删的文章可以随时捞回来。从数据库设计角度看软删除也让数据保留完整以后想统计历史发布量或做审计都有据可查。4. 避坑与常见问题跑这个课设最容易翻车的五个地方4.1 Tomcat起不来或者404现象双击Run后浏览器直接404或者Tomcat启动报ClassNotFound。原因最常见的是项目没有正确部署到Tomcat的webapps目录或者web.xml里servlet映射路径和表单action路径对不上。还有一种是Tomcat 10不兼容javax.servlet导致的——Tomcat 10已经把包名从javax.servlet换成了jakarta.servlet老课设代码直接跑必挂。解决这个项目是Servlet 3.0那代代码用Tomcat 8或Tomcat 9跑别用Tomcat 10以上。如果非要用新Tomcat就得全局替换import语句把javax改成jakarta。另外核对一下页面form的action值比如form actionaddNews对应WebServlet(/addNews)路径不一致404是必然的。4.2 登录后刷新又被踢回登录页现象登录明明成功了但点进任意一个页面又跳回login.jsp像是Session一直没生效。原因多半是Authorize过滤器把所有请求都拦了包括登录请求本身。如果过滤器逻辑写的是「没取到loginUser就去登录页」那登录请求在还没执行Servlet之前就被拦截自然永远登录不进去。还有一种情况是web.xml里配了过滤器却忘了在放行名单里加上login.jsp。解决在过滤器里明确放行/login、login.jsp、静态资源。另外检查登录成功后是否正确执行了session.setAttribute(loginUser, user)别只写了response.sendRedirect忘了写Session。4.3 拼写翻车NewsRealeseDao这个名字是个陷阱现象搜代码时按「Release」去搜怎么都搜不到全局替换也可能改错文件。原因源码里这个类名是NewsRealeseDao单词是「Realese」不是标准的「Release」。大概率是当初写代码的人手误但这个名称贯穿了项目所有模块包括import语句和实例化调用。如果图省事用IDE的全局替换功能统一改成Release可能会把注释、JSP里的引用、甚至数据库字段名一起误伤。解决第一次打开项目先确认这个命名后续所有搜索和新增代码都沿用Realese写法保持项目内部一致。真要改的话用IDE的Rename重构功能不要用简单的Find/Replace。4.4 中文乱码请求、响应、数据库三层必须一致现象JSP页面显示中文正常但往数据库插入后变成问号或者表单提交中文到Servlet后拿到的是一串乱码。原因三层字符集没对齐。浏览器发送的请求默认编码可能是ISO-8859-1数据库连接URL没带characterEncodingutf8JSP页面本身也没设pageEncoding。解决在Servlet开头加request.setCharacterEncoding(utf-8)写数据库连接串时带上useUnicodetruecharacterEncodingutf8建库建表统一用utf8mb4。如果是Tomcat 8以上POST请求默认已经是UTF-8了但GET请求的URL参数还是可能有乱码要检查Tomcat的server.xml里Connector是否配置了URIEncodingUTF-8。4.5 删除后文章在首页还能看到现象点删除按钮数据库里看is_deleted已经变成1了但首页列表里这篇文章依然显示。原因列表查询的SQL没加过滤条件。如果底层的查询方法是SELECT * FROM t_news那么不管is_deleted是0还是1全都会被查出来。这是软删除项目里最典型的翻车点——删了和没删一样「回收站」形同虚设。解决把列表查询SQL统一改成WHERE is_deleted0回收站列表单独写WHERE is_deleted1。同时对findById、findAll、searchNews等所有查询方法逐一排查确保每个查询都知道自己该查哪个状态的数据。5. 二次开发从课设骨架到可演示项目的四步改造5.1 第一步把直连JDBC换成Druid连接池课设版本的DAO里几乎都是每次操作前DriverManager.getConnection用完关掉。本地跑没问题但答辩演示时如果开了多个页面连接开销会让页面卡顿。常见做法是引入Druid连接池用一份db.properties加一个工具类搞定driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/newsdb?useUnicodetruecharacterEncodingutf8 usernameroot password123456 initialSize5 maxActive20参数说明initialSize是启动时预创建的连接数maxActive是最大连接数高并发场景下连接不够会自动排队等待。改造逻辑是把DBUtil.getConnection()的底层实现换成从连接池取连接业务代码不用动。5.2 第二步密码不能明文存t_user表里的password如果直接是明文答辩时老师问一句「密码怎么存的」很难答上来。常规改法是加盐MD5给每个用户生成一个随机盐值用盐和原始密码拼起来做摘要public static String md5WithSalt(String raw, String salt) throws Exception { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest((raw salt).getBytes(utf-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); }这段代码的原理是先拼接盐值再计算摘要避免直接拿彩虹表反查原始密码。盐值可以随机生成后和密码摘要一起存进数据库登录时取出该用户的盐再做一次同样的计算比对结果。5.3 第三步页面套一个Bootstrap模板原版JSP几乎没有CSS框架看起来比较素。二次开发时可以把Bootstrap的CDN链接加到公共头部模板里给表格加上table table-bordered类给按钮加上btn btn-primary半小时就能让界面脱胎换骨答辩演示的观感提升非常明显。5.4 第四步改造后的快速验证改完代码我习惯按这个顺序过一遍管理员登录能进后台看到所有文章普通用户登录能看列表、发文章、改自己的文章但改不了别人的普通文章删除后首页消失、回收站出现回收站里点恢复文章回首页登出后直接访问管理链接会被Authorize拦回登录页。这五条链路全通项目就能安心交出去了。第一次做这类改造时我吃过亏盐值随手写了个常量后来发现同一套盐下两个用户密码居然一样。从那次以后我每次动代码前都先把数据库备份脚本跑一遍密码相关的工具类写完后强制检查一遍盐值来源。希望帮到你。本文还有配套的精品资源点击获取