十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BS架构JavaWeb招聘网站实战:JSP+Servlet+MySQL从设计到部署

BS架构JavaWeb招聘网站实战:JSP+Servlet+MySQL从设计到部署 简介这是一份基于JavaWebJSPMySQL的BS架构人才招聘网站设计与实现源码包面向计算机专业毕业设计/课程设计学生以及想通过完整项目学习传统Java Web开发的入门开发者。包内含全套可运行源码和详细配套文档覆盖前台首页、网站导航、职位信息列表、用户注册、后台登录、职位信息管理、退出后台管理等核心模块并附带SQL数据库脚本便于本地部署和二次开发。压缩包共930个文件以gif、js、html、jar、jsp、java等类型为主包含页面交互、前端静态资源、后端逻辑与依赖库文件构成直观反映了经典JSP项目的目录规范整体约19.32MB。已有206人学习下载对希望快速掌握JSPServletMySQL开发流程的人来说是一份能直接运行、边看文档边变实践的优质参考。1. BS架构下的人才招聘网站从“能跑”到“可以用”的JavaWeb系统用 JavaWeb JSP MySQL 构建 BS 架构的人才招聘网站是课设、毕设和中小企业快速搭建内部招聘平台最常见的组合。这套技术栈的意义在于浏览器只负责渲染 JSP 生成的 HTMLServlet 承担请求分发与参数校验MySQL 存放用户、职位与投递数据三层职责清晰。大多数拿到这个标题源码包的人真正想做的事无非四类读懂已有代码、改掉一两个 bug、换掉数据库连接配置再顺手做点二次功能开发。这个系统的体量恰好适合把 JSP 标签、JDBC 封装、Servlet 生命周期和 SQL 优化串起来看一遍难度不大坑却不少。2. 招聘网站的BS架构设计与技术选型JSP、Servlet与MySQL的分层边界2.1 为什么这门技术组合还值得在 BS 架构里用BS 架构的核心是客户端无状态、服务端统一管理会话与数据。JavaWebJSPMySQL 的好处在于JSP 负责动态渲染 HTML浏览器端不需要引入 Node 或 Webpack 那套构建链。Servlet 负责接收 HTTP 请求并在服务端完成跳转MySQL 负责把职位、简历、投递记录持久化。相比直接把 SQL 写在 JSP 里这个组合最大的价值是 Servlet 作为“中间人”把业务逻辑从页面里剥离开。这里要澄清一个常见误区早期很多 JSP 项目把业务代码全写进% %脚本块里页面一改就触发 JSP 编译错误维护成本非常高。如果自己从零写至少拆成 Servlet DAO 两层Servlet 只取参数、调服务、做跳转DAO 负责 JDBC 数据访问。这样后面加“导出 Excel”或者“批量撤回投递”这类功能时不用在十几个 JSP 文件里翻找业务代码。2.2 三层职责划分与一次完整请求的流转路径一次“求职者搜索职位”的请求在各层的流转很直白层次典型文件职责边界表现层job_list.jsp、login.jsp接收表单数据渲染 HTML展示提示信息控制层JobListServlet、LoginServlet取参数、校验入参、调用 DAO、跳转或转发数据层JobDao、UserDao、DBUtil执行 SQL、封装结果集、处理连接事务对应到运行时请求路径是浏览器提交GET /job/list?keywordjavapage2Tomcat 根据 web.xml 里的映射找到 JobListServletServlet 解析 keyword 和 page调用 JobDao 执行分页 SQL把结果放进 request 域再forward到 job_list.jsp由 JSP 输出完整 HTML。这里有个容易忽略的点重定向sendRedirect会丢 request 里的数据所以列表页必须用 forward 转发。2.3 项目结构 WEB-INF 约束与 JavaWeb 配置要点拿到源码包先看目录结构判断这个项目到底能不能扩展。合适的划分是 controller、service、dao、model、util 五类包页面单独放 web 目录下。我一般先找 WEB-INF/web.xml再确认页面是否被 Servlet 转发访问。recruit-web/ ├── src/ │ ├── com/recruit/ │ │ ├── controller/ # Servlet 层接收请求并分发 │ │ ├── service/ # 业务逻辑层处理事务和状态 │ │ ├── dao/ # JDBC 数据访问SQL 集中管理 │ │ ├── model/ # 实体类对应数据库表字段 │ │ └── util/ # DBUtil、MD5Util、PageUtil │ └── db.properties # 数据库连接配置 ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器、欢迎页 │ │ └── lib/ # 第三方 jar 包 │ ├── jsp/ │ │ ├── job/ # 职位模块页面 │ │ ├── resume/ # 简历模块页面 │ │ └── admin/ # 后台管理页面 │ └── index.jspWEB-INF 下的 JSP 页面不能被浏览器直接通过 URL 访问只能经 Servlet forward 进入。所以很多新手把login.jsp放进 WEB-INF 后直接访问就报 404这是 JavaWeb 配置阶段最常见的错误。WEB-INF/lib 放的是 mysql-connector-java、jstl 这类依赖包用 Maven 构建的话这里不需要手动放 jarwar 打包时会把依赖装进来。JavaWeb 配置里另一个容易忽略的是编码过滤器中文参数在 GET 和 POST 两条路径上的乱码原因不一样。POST 乱码靠request.setCharacterEncoding(UTF-8)解决GET 请求是 Tomcat 在server.xml的 Connector 上解析 URI需要单独加过滤器或改 URIEncoding。简单做法是在 web.xml 里注册 Tomcat 自带的编码过滤器url-pattern 设为/*同时 POST 和 GET 的乱码都能兜住。3. MySQL数据库建模招聘网站的表设计、索引与常用SQL3.1 用户表、企业表与职位表的 DDL角色字段怎么定招聘网站的常见做法是六到八张表用户表、企业表、职位表、简历表、投递记录表再加一张公告表。管理员、企业账号、求职者账号可以统一放进一张t_user用 role 字段区分不需要额外做“角色表 用户角色中间表”。中间表在权限系统里是标准设计但在这里会让登录 SQL 多三次 JOIN性能和代码复杂度都划不来。CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5摘要, role TINYINT NOT NULL DEFAULT 2 COMMENT 0管理员,1企业,2求职者, company_id INT DEFAULT NULL COMMENT 企业账号关联公司, status TINYINT DEFAULT 1 COMMENT 1有效,0冻结, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_company ( id INT PRIMARY KEY AUTO_INCREMENT, company_name VARCHAR(120) NOT NULL, industry VARCHAR(50) COMMENT 行业, scale VARCHAR(20) COMMENT 规模, address VARCHAR(200), introduction TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_job ( id INT PRIMARY KEY AUTO_INCREMENT, company_id INT NOT NULL, title VARCHAR(100) NOT NULL COMMENT 职位名称, salary_min INT COMMENT 薪资下限单位K, salary_max INT COMMENT 薪资上限单位K, city VARCHAR(50), edu_require VARCHAR(20) COMMENT 学历要求, work_years VARCHAR(20) COMMENT 经验要求, description TEXT, status TINYINT DEFAULT 1 COMMENT 1招聘中,0下线, publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_company (company_id), INDEX idx_title (title), INDEX idx_publish (publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;角色用 TINYINT 而不是字符串占 1 字节查询时用WHERE role 2比WHERE role jobseeker更快。企业用户通过company_id关联公司表职位表再通过company_id关联公司这样企业名称只存一份不会出现职位列表里公司名各写各的脏数据。这里故意没有建物理外键删除企业时外键会强制检查子表手写 SQL 的项目里顺序一错就报错外键的维护成本大于收益关联一致性靠业务层的 Service 保证。3.2 简历表与投递记录表多对多关系的落地写法一个用户可以有多份简历但简化版系统通常只做一份主简历。简历表记录教育背景、工作年限、期望薪资和技能描述用user_id指向用户表。投递记录则是“简历”和“职位”之间的多对多关联表除了两个外键还带状态字段。CREATE TABLE t_resume ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), edu_degree VARCHAR(20) COMMENT 最高学历, work_years TINYINT COMMENT 工作年限, expectation_city VARCHAR(50), expectation_salary VARCHAR(20), skill_desc TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_delivery ( id INT PRIMARY KEY AUTO_INCREMENT, resume_id INT NOT NULL, job_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0已投递,1已查看,2邀请面试,3不合适, remark VARCHAR(200) COMMENT 面试备注或反馈, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_resume_job (resume_id, job_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_resume_job这个唯一键是整个投递逻辑的关键它保证同一份简历对同一个职位只能投一次。如果只在 Servlet 里先SELECT再INSERT并发请求下两条线程都查不到记录就会插出重复数据。有了唯一键兜底直接插入数据库会拒掉第二条程序捕获 Duplicate entry 异常后再抛提示即可。3.3 招聘网站高频查询 SQL模糊搜索、分页与统计搜索职位最常用的语句是LIKE %关键词%这个写法在数据量小时没什么感觉职位表到几十万行后前导百分号会让索引失效全表扫描的耗时随行数线性膨胀。先记住一个结论对小型招聘网站正确做法是控制 JOIN 范围、给常用过滤字段建联合索引而不是一上来就上全文检索。SELECT j.id, j.title, j.salary_min, j.salary_max, j.city, c.company_name, j.publish_time FROM t_job j INNER JOIN t_company c ON j.company_id c.id WHERE j.status 1 AND (j.title LIKE CONCAT(%, ?, %) OR c.company_name LIKE CONCAT(%, ?, %)) AND (? OR j.city ?) ORDER BY j.publish_time DESC LIMIT ?, ?;四个?分别是 keyword、keyword、city、city最后两个参数是 offset 和 pageSize。LIMIT的 offset 不能在 Servlet 里直接拿用户传的页码当参数正确算法是offset (page - 1) * size。城市条件写成(? OR j.city ?)比拼接两个 SQL 更干净DAO 层只需要传同一个 session 字符串。统计企业收到的投递量是管理后台的常用功能。这里用LEFT JOIN而不是INNER JOIN因为招聘中的企业即使一条投递都没有也要显示 0而不是被过滤掉。SELECT j.company_id, c.company_name, COUNT(d.id) AS delivery_count FROM t_job j LEFT JOIN t_delivery d ON j.id d.job_id LEFT JOIN t_company c ON j.company_id c.id WHERE j.status 1 GROUP BY j.company_id, c.company_name ORDER BY delivery_count DESC;COUNT(d.id)只统计真实存在的投递记录如果t_delivery没有匹配行左边记录也会保留COUNT 结果是 0。GROUP BY 后面带的列要和 SELECT 中非聚合列一致否则在高版本 MySQL 的 ONLY_FULL_GROUP_BY 模式下会直接报语法错误。4. JSPServlet实现核心业务链路登录、职位分页与简历投递4.1 登录认证的 Servlet 处理与 Session 生命周期登录逻辑是理解这套系统最适合的入口它把请求参数、DAO 查询、Session 存储和页面跳转串在一起。先看最常见的 Servlet 写法再补一个登录检查的过滤器。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); String role request.getParameter(role); User user userDao.findByLogin(username, password, role); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); String target 2.equals(role) ? index.jsp : admin/index.jsp; response.sendRedirect(target); } else { response.getWriter().write( scriptalert(用户名或密码错误);history.back();/script); } } }登录前就做 md5 摘要避免明文密码进入 SQL 语句也避免日志或异常堆栈里带出密码。role参数来自登录表单的下拉框找到用户后要及时把用户对象放进 Session后续所有页面都从 Session 里取当前登录人。登录成功后用sendRedirect而不是 forward是为了让浏览器地址栏变成目标页刷新时不会再次提交表单避免每次刷新都弹“重新发送表单”。登录检查 Filter 是第二道防线如果用户没有登录就访问受保护目录直接重定向到登录页。一个过滤器能让整站受保护不需要每个 Servlet 都重复判断 Session 是否为空。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 loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }过滤器最常踩的坑是放行规则没配好。静态资源、登录页和登录 Servlet 需要排除在外否则正常用户也会被拦回来。url-pattern用/job/*、/resume/*这类精确目录范围比用/*到处放行更安全。4.2 职位列表的分页查询与 JSP 渲染参数计算职位列表页是招聘网站的流量入口涉及三个要素页码解析、DAO 分页查询、JSP 端循环输出。先看控制层的分页代码。WebServlet(/job/list) public class JobListServlet extends HttpServlet { private JobDao jobDao new JobDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String keyword request.getParameter(keyword); String pageStr request.getParameter(page); String sizeStr request.getParameter(size); int page pageStr null ? 1 : Integer.parseInt(pageStr); int size sizeStr null ? 8 : Integer.parseInt(sizeStr); // 只允许正向翻页防止负数页码把 offset 算成负数 page Math.max(page, 1); int offset (page - 1) * size; ListJobVO list jobDao.queryPage(keyword, city, offset, size); int total jobDao.countPage(keyword, city); int totalPages (total size - 1) / size; // 超过最大页时回退到最后一页避免空列表 if (page totalPages) { page totalPages; offset (page - 1) * size; list jobDao.queryPage(keyword, city, offset, size); } request.setAttribute(jobList, list); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/jsp/job/list.jsp).forward(request, response); } }分页参数计算容易出错的地方是两个用户直接手改 URL 传page0或page10000第一种用Math.max(page, 1)挡掉第二种在查出总页数后再做一次越界回退。查询列表和查询总数必须用同一组 WHERE 条件否则页数对不上。JSP 侧用 JSTL 的c:forEach渲染避免在页面里写 Java 脚本块。c:if 控制上一页下一页是否显示需要往前端模板传当前页和总页数。c:forEach items${jobList} varjob div classjob-item h3a href${pageContext.request.contextPath}/job/detail?id${job.id} ${job.title}/a/h3 p${job.companyName} | ${job.city} | ${job.salaryMin}K-${job.salaryMax}K/p a classbtn href${ctx}/delivery/add?jobId${job.id}立即投递/a /div /c:forEach c:if test${currentPage 1} a href${ctx}/job/list?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} / ${totalPages} 页/span c:if test${currentPage totalPages} a href${ctx}/job/list?page${currentPage 1}下一页/a /c:if${ctx}已经在 Servlet 或页头用pageContext.request.contextPath注入过作用是拼出项目上下文根路径。直接写/job/list在部署到根目录时没问题一旦 WAR 包改名后发布到同端口硬编码路径就会 404。页面里不要直接访问job对象的内部字段JSP 中通过 EL 表达式调用的是 getter 方法字段名大小写写错会直接报 PropertyNotFoundException。4.3 简历投递的幂等控制与不可重复投递提示投递动作由一个 Servlet 接收请求检查登录态和简历完整性最后调用 DAO 插入投递记录。这里需要同时处理两个问题用户没登录直接投递、用户重复投递。先写 DAO 层。public boolean addDelivery(int resumeId, int jobId) { String sql INSERT INTO t_delivery (resume_id, job_id) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, resumeId); ps.setInt(2, jobId); return ps.executeUpdate() 0; } catch (SQLException e) { if (e.getErrorCode() 1062) { // 1062 是 MySQL 的唯一索引冲突错误码说明重复投递 return false; } e.printStackTrace(); return false; } }常见错误码含义本项目中对应场景1045数据库用户名或密码错误db.properties 配置错1062唯一键冲突同一简历重复投递同一职位1049数据库不存在建库脚本没执行这套代码并不需要进入数据库先查一次再说INSERT 唯一键冲突时直接返回 false使并发投递时也只有一条能成功这就是 MySQL 锁表之外更轻量的幂等方案。Comparator和页面层检查 Session 为空时跳登录页简历不存在时跳编辑页DAO 返回 false 时用history.back()提示不可重复投递配合 alert 弹窗处理。5. 部署到Tomcat的版本选择与上线验证技巧5.1 WAR 包构建与 Tomcat 9 / 10 版本差异项目在本机跑通和正式部署之间隔着一个版本兼容问题。这套 JSP Servlet 源码如果用的是javax.servlet包名建议直接选 Tomcat 9。Tomcat 10 之后把命名空间迁到了jakarta.servlet老代码编译通过但运行时报ClassNotFoundException这是从黑马 JavaWeb 笔记到实际部署最常见的错位点。# Maven 打 war 包并跳过测试 mvn clean package -DskipTests # 拷贝到 Tomcat webapps 目录 cp target/recruit.war /opt/tomcat/webapps/ # 启动 /opt/tomcat/bin/startup.sh tail -f /opt/tomcat/logs/catalina.outWAR 包部署后 Tomcat 会自动解压出recruit/目录浏览器访问路径是http://IP:8080/recruit/。如果改完 JSP 后页面不生效先删掉tomcat/work/Catalina/localhost/recruit/下的 JSP 编译缓存目录这相当于前端强刷之外的 JSP 缓存层。5.2 用一个 bat 脚本带走 MySQL 每日业务数据部署在 Windows 服务器上的小型招聘网站最靠谱的备份方案不是装第三方客户端而是用计划任务每天跑一次 mysqldump。这样即使第二天误删数据也能在几分钟内恢复。echo off set MYSQL_HOMEC:\mysql-8.0.36-winx64\bin set BACKUP_DIRD:\backup\recruit_db set DB_NAMErecruit_db set DB_USERroot set DB_PASS123456 set YYYYMMDD%date:~0,4%%date:~5,2%%date:~8,2% %MYSQL_HOME%\mysqldump -u%DB_USER% -p%DB_PASS% --default-character-setutf8mb4 %DB_NAME% %BACKUP_DIR%\recruit_%YYYYMMDD%.sql forfiles /p %BACKUP_DIR% /m *.sql /d -15 /c cmd /c del path前两行 set 是 MySQL 安装目录和备份目录按实际环境改。%date:~0,4%取当前日期前四位做年份后面的~5,2和~8,2取月和日前提是系统日期格式为yyyy-MM-dd。首次运行前先执行echo %date%验证一下区域设置的格式不同取子串的位置要对齐。最后一行保留最近 15 天备份防止磁盘被 sql 文件塞满。执行这一脚本时遇到 1045 错误先检查账号密码是否在命令行里有特殊字符备份出的文件用show tables;快速验证是否完整再考虑加入 Windows 任务计划。5.3 上线后的三条验证路径部署完成后别急着点功能按顺序走三条检查路径。第一次检查 Tomcat 日志与 JSP 编译产物重启后访问首页并观察 work 目录下是否生成了对应的 class 文件第二次用 Navicat 或命令行执行SHOW INDEX FROM t_delivery;确认唯一键和常用查询的索引都存在第三次用两个不同浏览器同时登录验证 Session 不会互相踢掉再在其中一个窗口连续点击两次投递按钮确认第二条请求被拦截。做完这三项整个系统从数据库到页面才算真正连通。本文还有配套的精品资源点击获取
返回列表