简介:一套基于 jsp+js+servlet+jdbc+mysql 原生界面开发的后台管理系统源码,适合 Java Web 初学者、课程设计或毕业设计参考。项目不依赖复杂前端框架,完整实现登录、注册、用户/产品增删改查、分页查询、图片上传、退出及未登录过滤器拦截等后台常见功能,可帮助读者理解 servlet+jdbc 的请求处理与数据库交互流程。压缩包共92个文件,含14个java与12个jsp源码、28个class编译文件,以及jar依赖、xml配置、数据库脚本和docx导入步骤说明,整体仅3.04MB,便于快速下载与本地部署。附带的 mydb.sql 可直接建库,工程导入步骤与运行必读文档能减少环境配置弯路。已有3371人学习下载,适合需要完整可运行后台项目或希望逐模块拆解原生 Java Web 开发的读者。
1. 这套 jsp+servlet+jdbc+mysql 后台管理系统:老技术栈为什么还能救急
去年我带的一个练手小组把课程设计题目定为“产品后台管理系统”,要求不许用 Spring Boot,必须用 Java 原生的 jsp+servlet+jdbc+mysql 把界面和逻辑写出来。折腾两周后他们交上来的东西,和这份 xianfengProject 的结构几乎一样:登录、注册、产品增删改查、分页、图片上传、过滤器拦 session。我当时把这套源码当参考答案发给组里新人时说了一句话:把这几条链路跑通,再去碰框架,脑子里才有“请求到底是怎么穿过后端”的那张地图。现在很多培训项目上来就是 Spring Boot+MyBatis-Plus,模板一生成,servlet 和 jdbc 反而成了黑匣子。这套源码值钱的地方,恰恰是它把黑匣子拆开给你看:分页用 limit 怎么算、图片上传怎么落盘、过滤器怎么拦未登录用户。适合三类人:刚学完 JavaWeb 想找完整闭环案例的在校生,准备 java 面试想复习 servlet 和 jdbc 底层细节的求职者,以及需要一套老技术栈管理系统改改就交差的在职工程师。我照着跑通一次,全程没遇到玄学问题,但确实有几个坑,后面单开一章说。
2. 项目功能与原理解剖:登录、产品管理、过滤器是怎么串起来的
2.1 五个功能模块的请求映射关系
这套系统的主入口是 index.jsp,上面放了登录和注册两个入口。登录成功之后 session 里会写入用户标识,然后跳转到产品管理总页面。产品管理页承载了最重的业务:查询某个产品、添加、修改、批量删除、查看详情、分页展示。退出功能就是清 session 回主页,逻辑最简单,但承载着整个权限闭环。
从请求路径来看,整个系统的流转关系大概是这样一张映射表:
| 请求路径 | 对应处理 | 说明 |
|---|---|---|
| /index.jsp | 主页展示 | 未登录时的默认落地页 |
| /login.do | LoginServlet | 校验用户名密码,成功则写入 session |
| /register.do | RegisterServlet | 新增用户,写入 t_user 表 |
| /product/list.do | ProductServlet(list) | 分页查询产品,默认 pageIndex=1 |
| /product/add.do | ProductServlet(add) | 添加产品,涉及图片上传 |
| /product/update.do | ProductServlet(update) | 修改产品 |
| /product/batchDelete.do | ProductServlet(batchDelete) | 批量删除 |
| /logout.do | LogoutServlet | 清除 session 后 redirect 到 index.jsp |
我在实际复现时会先画这张表,再去对照源码里的 servlet 映射。因为原生 servlet 项目没有框架里那种注解路由(或者只在 servlet 上用 @WebServlet),通读一遍 web.xml 或注解,整个功能边界就清楚了。
2.2 目录结构里藏着的老工程痕迹
压缩包解开后的内容很有意思,它同时保留了 .idea 和 .mymetadata、.project、.classpath,说明这份源码在 IntelliJ IDEA 和 MyEclipse 里都被打开过。xianfengProject.iml 是 IDEA 的模块文件,out 目录是 IDEA 编译输出目录,WebRoot 是 web 根目录。这些细节对复现有用:用 IDEA 导入时直接 Open 这个目录,IDEA 会读取 .iml 恢复模块结构;用 Eclipse/MyEclipse 导入时则看 .project 和 .classpath。
- src 目录放 Java 源码:servlet、dao、bean、util
- WebRoot 目录放 jsp 页面、WEB-INF/web.xml、js、css、上传图片目录
- 压缩包内附带的 mydb.sql 是初始化脚本,工程导入步骤.docx 和运行必读.txt 是作者写的操作说明
我一般建议先打开运行必读.txt 再动手,因为这种老工程最容易翻车的就是环境版本不匹配。正文里写了 jdk1.7、tomcat7、mysql5.5/5.6/5.7,这三个版本必须对齐,别脑子一热装个 tomcat10,servlet 包名都变了,代码直接编译不过。
2.3 mydb.sql 里的核心表结构设计
这套系统只需要两张核心表:用户表 t_user 和产品表 t_product。产品表要存图片路径,所以单独留了一个 image_path 字段;价格用 DECIMAL 而不是 FLOAT,避免精度问题。下面是按常规设计补全的建表 SQL,和源码里 mydb.sql 的思路一致:
CREATE DATABASE IF NOT EXISTS xianfeng_db DEFAULT CHARSET utf8mb4; USE xianfeng_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, description TEXT, image_path VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO t_user(username, password) VALUES('admin', '123456');这里有两个细节值得注意。第一,字符集要 utf8mb4 而不是 utf8,否则插入 emoji 或生僻字时 jdbc 连接 mysql 会直接报 Incorrect string value。第二,密码字段设计成 VARCHAR(50) 是课程设计常见做法,生产环境绝不这么干,至少得加盐哈希。我复现时只是为了跑通流程,所以沿用原设计的明文存储,但如果你要拿这套资源改造成真正的工程,密码存储是第一件必须改的事。
3. 从压缩包到跑通:环境、导入、建库、部署四步走
3.1 环境版本对齐,少一步后期就是玄学现场
老工程最怕的就是版本漂移。项目说明明确写了 jdk1.7、tomcat7、mysql5.5/5.6/5.7。我本地用的组合是 jdk1.7 + tomcat7.0.94 + mysql5.7.44,一次跑通。如果你电脑上已经装了高版本 JDK,比如 JDK8 或 JDK11,编译目标可以调成 1.7 来兼容,但 tomcat 建议老老实实用 7.x。tomcat8.5 以上对老式 jsp 的隐式对象处理有细微差异,虽说大概率没问题,但没必要为了一个复现去赌兼容性。
还有 jdbc 驱动版本:mysql5.7 用 mysql-connector-java-5.1.49.jar 最常见,驱动类名是 com.mysql.jdbc.Driver。如果你手头是 8.x 的驱动包,驱动类名要换成 com.mysql.cj.jdbc.Driver,连接串还要额外加 serverTimezone 参数。这一步是 jdbc 连接 mysql 的经典坑,后面避坑章细说。
3.2 用 IDEA 导入工程
源码自带 .idea 目录和 iml 文件,所以用 IntelliJ IDEA 打开最省事。不是新建工程,而是 File -> Open 直接选中 xianfengProject 目录,IDEA 会根据 iml 恢复模块配置。打开后第一件事是检查 Project Structure:
Project SDK: 选 1.7 或按需调成 1.8(编译级别 Level 7) Project language level: 7 - Diamonds, ARM, multi-catch 等 Modules -> Dependencies: 确认 lib 下的 mysql-connector 等 jar 已加入之后配置 Tomcat:Run -> Edit Configurations -> 左上角 + -> Tomcat Server -> Local,在 Deployment 页签里添加 Artifact,Application context 填 /xianfengProject。这个 context path 决定了你访问地址是 http://localhost:8080/xianfengProject/index.jsp。
逻辑说明:老式 web 工程在 IDEA 里的部署方式是 Exploded(解压目录),不是 war 包。这样做的好处是 jsp 修改后刷新页面就能看到,不用重新打包。你在 Deployment 里看到的 artifact 类型应该是 Web Application: Exploded。
3.3 初始化数据库与账号配置
打开命令行,进入 mysql 的 bin 目录,执行:
mysql -u root -p < mydb.sqlmydb.sql 在压缩包根目录,建议先用记事本打开看一眼,确认里面的库名和表名。如果脚本里没有 CREATE DATABASE,你需要手动先建库,再使用。导入成功后在 mysql 命令行执行 SHOW TABLES; 能看到 t_user 和 t_product 就说明数据库这边齐活了。
然后打开源码里的 jdbc 配置文件,一般是 src 下名为 db.properties 或 jdbc.properties 的文件,改成你本地的账号密码:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/xianfeng_db?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=你的数据库密码参数说明:useUnicode=true 和 characterEncoding=utf8 这两个参数建议保留,否则从 mysql 读中文再写进 jsp 显示时容易出现乱码。端口 3306 如果你本地改过,也要同步换。这条连接串是 jdbc 连接 mysql 最经典的写法,很多框架的底层数据源本质上也在拼这么一串。
3.4 启动 Tomcat 并跑通第一个用例
启动前先确认四件事:tomcat 端口没被占、jdk 版本匹配、artifact 已经部署、mysql 服务是启动状态。然后点 IDEA 里的 Run 启动 Tomcat,控制台出现 Server startup 日志后,浏览器访问:
http://localhost:8080/xianfengProject/index.jsp首次访问能看到主页说明 web 应用没问题。然后用 admin/123456 登录,如果能进产品管理页,这就算跑通了。接下来建议立刻做两件事验证功能完整性:随便添加一个带图片的产品,然后在前台翻几页看分页是否正常。第一轮复现只要这两条链路通了,这套源码的核心功能就全部验证到位了。
4. 源码里值得一行行读的三个实现:分页、图片上传、过滤器
4.1 分页查询:limit 参数与页脚逻辑
原生 jdbc 做分页,思路比 MyBatis-Plus 的 IPage 要直接得多。核心就是 SQL 拼 limit,前端把当前页码传给 servlet,servlet 通过 request.getParameter 拿参数,dao 层计算偏移量后执行查询。下面是 ProductDao 分页方法的核心代码:
public List<Product> findByPage(int pageIndex, int pageSize) { List<Product> list = new ArrayList<Product>(); String sql = "SELECT id, name, price, description, image_path FROM t_product " + "ORDER BY id DESC LIMIT ?, ?"; int start = (pageIndex - 1) * pageSize; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, start); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setDescription(rs.getString("description")); p.setImagePath(rs.getString("image_path")); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; }代码逻辑:start 计算的是跳过的记录数,pageIndex 从 1 开始,第一页就是 limit 0, 10。ORDER BY id DESC 表示新品在前,后来我在改造时加了“默认按添加时间倒序”的需求,逻辑同理。参数说明:pageSize 是每页行数,一般在 servlet 里写死成常量,或者从前端下拉框传入,这里用 PreparedStatement 预编译也顺便防了 SQL 注入,是原生 jdbc 写法的标准姿势。
配套的还要有查询总记录数的 count 方法,页脚才能显示共 X 页:
public int count() { String sql = "SELECT COUNT(*) FROM t_product"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } return 0; }前端页脚计算总页数时,用 (total + pageSize - 1) / pageSize 代替除法向上取整,避免浮点误差。这是原生分页里最后一步,很多新手在这里翻车。
4.2 图片上传:servlet3.0 的 Part 接口
图片上传是整套系统里最有技术含量的环节。摘要里写了图片上传功能,实现上在 tomcat7 环境可以直接用 servlet3.0 的 Part 接口,不需要 commons-fileupload。product 表单里 input 标签要加 enctype="multipart/form-data",这是第一个必须注意的点。
@WebServlet("/product/add.do") public class ProductAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String name = request.getParameter("name"); Part part = request.getPart("image"); String fileName = getFileName(part); String uploadPath = getServletContext().getRealPath("/") + "upload"; File dir = new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } part.write(uploadPath + File.separator + fileName); String price = request.getParameter("price"); String desc = request.getParameter("description"); // 组装 Product 对象,调用 dao 层插入 t_product // image_path 字段存 "upload/文件名",方便页面拼接展示 response.sendRedirect("list.do?pageIndex=1"); } private String getFileName(Part part) { String header = part.getHeader("content-disposition"); String[] arr = header.split(";"); for (String s : arr) { if (s.trim().startsWith("filename")) { return s.substring(s.indexOf("=") + 2, s.length() - 1); } } return "default.png"; } }这段代码有几个关键细节。getRealPath("/") 返回的是 webapp 部署后的磁盘绝对路径,upload 目录要在第一次上传前创建,否则 part.write 会报 FileNotFoundException。getFileName 这个方法不能省,Part 对象没有直接提供原文件名方法,只能从 content-disposition 头里截取。图片存储路径建议存相对路径 "upload/文件名",页面 标签直接写 src="upload/xxx.png" 就能展示,这也是摘要里提到“jsp 图片如何定位”的常见解法。
4.3 过滤器:session 验证与放行路径
过滤器是这套系统安全机制的核心。如果用户没登录直接访问产品管理页面,必须被拦回 index.jsp。实现就是实现 javax.servlet.Filter 接口,在 doFilter 里检查 session:
@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); String ctx = request.getContextPath(); String path = uri.substring(ctx.length()); boolean isStatic = path.startsWith("/js/") || path.startsWith("/css/") || path.startsWith("/upload/") || path.endsWith(".js") || path.endsWith(".css"); boolean isPublic = path.equals("/index.jsp") || path.equals("/login.do") || path.equals("/register.do") || path.equals("/logout.do"); HttpSession session = request.getSession(false); boolean loggedIn = session != null && session.getAttribute("user") != null; if (loggedIn || isStatic || isPublic) { chain.doFilter(req, resp); } else { response.sendRedirect(ctx + "/index.jsp"); } } }这个过滤器的逻辑要仔细理解:放行三类请求——已登录用户的请求、静态资源、公开页面。没有登录且不满足放行条件的,重定向回 index.jsp。我见过很多新手写的过滤器只判断了 session,结果把登录页面和静态 JS 全都拦了,页面样式全丢或者死循环重定向。注意 getSession(false) 不要写成 getSession(),带参 false 不会自动创建 session,避免未登录用户每次访问都被强制生成 session,这是个资源浪费的隐形坑。
5. 避坑指南:复现这套源码最容易翻车的五个位置
5.1 环境部署与启动过程中的典型问题
现象一:执行 net start mysql 时提示服务名无效,或者服务正在启动后立刻停止。我遇到这台机器装的是 mysql5.7.44,之前用安装版装过一次,后来又解压了一份免安装版,注册表里的服务名根本没对上。原因:服务名不是默认的 mysql,而是你安装时指定的名称,可能是 MySQL57 或自定义的名字。解决:用管理员身份打开 CMD,执行 sc query 或直接到服务管理器里看服务名,再执行 net start 服务名;如果服务列表里没有,就用 mysqld --install 先注册服务再启动。
现象二:本地装了 JDK8,但源码在编译时报错,看不到 API 或语法级别不对。这套工程注释里写的是 jdk1.7,如果直接用 IDEA 默认 SDK 打开,编译级别可能带着 1.8 语法,servlet 代码没问题,但某些老 lib 会和 JDK8 产生 bytecode 版本冲突。解决:Project Structure 里把 Project SDK 定义为 1.7(没有就先装一个),Language level 选 7,Modules 里的 SDK 也同步改。如果你机器上只有 JDK8 不想再装旧的,把编译 target 调成 1.7 也能缓解大部分问题。
现象三:Tomcat 启动时端口被占用,报 SEVERE 级别的 Socket bind 失败。原因多半是上次 IDEA 非正常关闭,java.exe 进程还占着 8080。解决:CMD 执行 netstat -ano | findstr :8080 查到 PID,再 taskkill /PID 你的PID /F,杀完重启就好。血泪经验:不要一上来就把 tomcat 的 server.xml 端口改成 8081,治标不治本,而且 session 里的回跳地址不会自动适配新端口。
现象四:页面访问 404,显示源服务器未能找到目标资源的表示。原因一般是 artifact 没部署成功,或者 context path 不是 /xianfengProject。Tomcat 的部署机制里,war 或 exploded artifact 要挂在指定 context 下,访问路径是 ip:port/context/资源路径,少一层或多一层都会 404。解决:检查 Run Configuration 的 Deployment 页签,确认 artifact 已经添加且 Application context 写的 /xianfengProject,然后重启 Tomcat 重新部署。
现象五:数据库连接报 java.sql.SQLException: Unknown database。使用 mysql -u root -p < mydb.sql 导入前,如果 mydb.sql 里没有 CREATE DATABASE,或者当前用户没权限建库,表就落在系统库里甚至没落进去,但 JDBC URL 里却写了 xianfeng_db,结果就是连接报 Unknown database。解决:先手动执行 CREATE DATABASE xianfeng_db DEFAULT CHARSET utf8mb4,再进入该库导入脚本,或者直接在 mysql 命令行用 source 方式导入 mydb.sql。
5.2 代码运行与数据层面的常见踩坑点
现象一:前端中文乱码,界面显示“锟斤拷”。原因有三个位置必须同时设置 UTF-8:JSP 页面顶部的 pageEncoding、servlet 里 request.setCharacterEncoding("utf-8")、jdbc 连接串里的 characterEncoding=utf8。少一个就乱,这是老项目的连环坑。解决:三个位置全部补齐,product 添加页面和注册页面的表单都过一遍。
现象二:图片上传后 标签显示裂图,刷新后图片丢失。原因:开发环境用 getRealPath 把图片写到 tomcat 部署目录下的 upload 里,但 IDEA 重启或 clean 后整个部署目录被重建,上传的文件随之被删除。这是 tomcat explode 部署的经典副作用。解决:把上传路径改成工程外目录,比如 D:/upload,然后通过映射或单独 servlet 提供图片访问。如果只是跑课程设计,接受重启丢图片的现实也行,但心里要清楚这个边界。
现象三:过滤器把登录页的 css/js 也拦截了,页面虽然能打开但是光秃秃的没有样式。原因:@WebFilter("/*") 匹配所有请求,css 和 js 也在拦截范围内,session 为空时全部重定向到 index.jsp,最终浏览器加载静态资源时循环重定向。我在 4.3 节里已经写了放行逻辑的完整代码,把路径判断提前到 session 判断之前,问题就解决了。
现象四:分页点下一页时说 pageIndex 为 null 的 NumberFormatException,或者总页数算出来是 1 但数据有 50 条。原因:servlet 里直接 Integer.parseInt(request.getParameter("pageIndex")),第一页时链接没带参数就报空指针;总页数用除法直接取整,10 条数据 pageSize=10 时总页数算成 1,恰好第 11 条就消失了。解决:parseInt 前先判空并给默认值 1,总页数用 (total + pageSize - 1) / pageSize 计算。
6. 进阶改造:把 jdbc 直连换成 Druid 连接池,顺便解决连接资源释放问题
如果你准备把这份源码改造成能拿去实习面试里讲的工程,第一个要动刀的地方绝对不是界面美化,而是数据源。原生 jdbc 最尴尬的场景就是每次请求都 DriverManager.getConnection,用 complete 关 Connection、Statement、ResultSet,代码丑且容易漏关。我的习惯是引入 Druid,把数据源初始化挤到 static 块里,让整个 webapp 只维护一个连接池。
先在 lib 目录放 druid-1.1.24.jar,然后在 src 下加一个 DBUtil 改造版:
import com.alibaba.druid.pool.DruidDataSource; public class DBUtil { private static DruidDataSource dataSource; static { dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/xianfeng_db?useUnicode=true&characterEncoding=utf8"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(3); dataSource.setMaxActive(10); dataSource.setMaxWait(3000); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }逻辑说明:Druid 的 setInitialSize 是启动时预建的连接数,setMaxActive 是池中最多同时活跃的连接数,setMaxWait 是拿不到连接时最长等待毫秒数。这样就省去了每个 dao 方法里 DriverManager.getConnection 的重复代码,连接获取速度也快了一个数量级。原有 dao 层的 try-with-resources 不用动,因为 Druid 的 getConnection 返回的是代理连接,调用 close 只是归还池子,不会真的断开。dao 层代码里所有 Class.forName 加载驱动的语句都可以删掉了,数据源初始化时已经完成。
接下来把这段改动跑一遍回归:登录、添加产品、上传图片、分页浏览、退出,五个链路全部过一遍,重点观察 Tomcat 控制台有没有连接泄漏提示。改完你会发现两件事,第一,页面响应速度在连续点击分页时明显变快,因为连接复用而不是每次新建;第二,Tomcat 重启时再也不会因为连接未释放报警。从那以后,我每次拿到这种课程设计源码,都会强制先做一遍数据源改造再谈功能,顺手也会把过滤器放行路径完整走查一遍。这套方法希望你也能用上,帮你少走弯路。
本文还有配套的精品资源,点击获取