简介:这份资源是面向高校计算机相关专业学生与Java Web初学者的企业宣传网站毕业设计完整源码包,基于Java与JSP技术栈实现,可用于课程设计、毕业设计参考或Java Web入门实战练习。压缩包共收录879个文件,约21.27MB,涵盖gif、jpg、png等图片素材,js、css、html等前端资源,以及jsp页面、java类文件、jar依赖包、xml配置与数据库脚本等,完整呈现了从前端展示到后端业务逻辑的项目结构。目前已有164人学习下载。源码中可看到公司简介、产品展示、新闻发布、联系我们等典型企业站点模块的实现方式,读者可借此理解Servlet与JSP标签的配合、MVC分层思路、数据库连接与配置文件的组织,并对照实际项目学习页面布局、交互效果与服务器端编程的落地写法,对提升Java Web开发能力与完成同类课题具有较高参考价值。
1. 企业宣传网站为什么还在用 JSP:一个被低估的毕设选题
很多同学一提到毕业设计选 Java Web,第一反应就是 Spring Boot 加 Vue 前后端分离,觉得 JSP 是上个时代的东西,拿不出手。但我带过几届学生的实际情况恰恰相反:基于 Java+JSP 的企业宣传网站,是计算机毕业设计里性价比最高、最容易跑通、答辩时最不容易被问穿的选题之一。原因很直接——企业宣传网站的业务模型天然简单,无非是首页展示、产品列表、新闻动态、关于我们、留言板、后台管理这几块,没有复杂的并发和分布式要求,而 JSP 的请求响应模型、内置对象、标签库刚好能把「页面展示 + 数据读取」这件事讲得清清楚楚。
这篇文章要解决的,就是拿到「基于 Java+JSP 的企业宣传网站毕业设计」这个题目之后,怎么从零把它做出来、源码结构怎么组织、数据库怎么设计、部署怎么落地,以及答辩前哪些坑必须提前填掉。适合两类人:一是正在做基于 JSP 的毕设选题、需要一套能跑通的完整思路的同学;二是 Java 基础刚学完、想找一个真实项目练手 JSP 入门和 Java Web 头歌实训内容的初学者。我不会给你画大饼,只讲我实际搭过、部署过、被答辩老师追问过的那些细节。
2. 技术选型与工程结构:为什么这套组合现在依然能打
2.1 为什么是 JSP 而不是纯 Servlet 或 Spring Boot
先把这个选型问题说透,因为答辩时老师十有八九会问。纯 Servlet 的问题是所有 HTML 都得在 Java 代码里用out.println拼字符串,一个产品列表页能写出几百行,维护起来是灾难。Spring Boot 的问题是对于企业宣传网站这种体量,引入依赖注入、自动配置、Maven 多模块,学习成本和调试成本陡增,一个刚学完 Java 基础的同学很容易卡在环境配置上出不来。
JSP 的定位刚好卡在中间:它本质还是 Servlet,容器会把它编译成 Servlet 执行,但写法上允许你在 HTML 里嵌入 Java 代码和标签,页面结构和数据填充能放在一起写。对于「展示为主、交互为辅」的企业宣传网站,这个模型非常顺手。常见做法是 JSP 负责视图渲染,Servlet 负责接收请求和调度,JavaBean 负责数据封装,DAO 负责数据库访问,这就是经典的 Model1 或 Model2(MVC)结构。
我一般推荐用 Model2,也就是 MVC 三层:JSP 只做展示,Servlet 做控制器,JavaBean + DAO 做模型。这样答辩时你能清楚地说出「视图、控制、模型分离」,比 Model1 那种 JSP 里直接写数据库代码要专业得多,也更容易扩展。
2.2 工程目录结构怎么组织
一个能直接跑、结构清晰的企业宣传网站工程,目录大致是这样组织的。这里以 Eclipse 动态 Web 项目为例,IDEA 新建 JSP 项目的结构逻辑一致,只是目录名略有差别。
EnterpriseSite/ ├── src/ │ └── com/company/ │ ├── dao/ // 数据访问层 │ │ ├── NewsDao.java │ │ └── ProductDao.java │ ├── model/ // 实体类 JavaBean │ │ ├── News.java │ │ └── Product.java │ ├── servlet/ // 控制器 │ │ ├── NewsServlet.java │ │ └── ProductServlet.java │ └── util/ // 工具类 │ └── DBUtil.java ├── WebContent/ │ ├── css/ │ ├── js/ │ ├── images/ │ ├── WEB-INF/ │ │ ├── web.xml // 部署描述符 │ │ └── lib/ // 第三方 jar │ ├── index.jsp // 首页 │ ├── news.jsp // 新闻列表 │ ├── product.jsp // 产品展示 │ └── admin/ // 后台管理页面 └── ...这个结构的关键点在于:WEB-INF目录下的内容浏览器无法直接访问,所以把web.xml和第三方 jar 放这里是安全的;而 JSP 页面放在WebContent根目录下才能被直接请求。src下的包按职责分层,答辩时打开工程一眼就能看出你的架构意识。
2.3 数据库表设计:五张表撑起整个网站
企业宣传网站的数据模型不复杂,五张表基本够用。下面给出建表语句,字段类型和长度都是我在实际项目里验证过的,可以直接抄。
-- 新闻资讯表 CREATE TABLE news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, content TEXT, author VARCHAR(50), publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, category VARCHAR(50) ); -- 产品表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, description TEXT, price DECIMAL(10,2), image_url VARCHAR(500), category VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ); -- 留言表 CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), email VARCHAR(100), content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, is_read TINYINT DEFAULT 0 ); -- 单页内容表(关于我们、联系方式等) CREATE TABLE page_content ( id INT PRIMARY KEY AUTO_INCREMENT, page_key VARCHAR(50) UNIQUE, page_title VARCHAR(200), content TEXT, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );参数说明:news表的category用来区分「公司新闻」「行业动态」,前端可以按分类筛选;product表的image_url存图片相对路径而不是二进制,避免数据库膨胀;admin表的password长度给到 100,是为了后面存 MD5 或加盐哈希留空间,不要用明文;page_content表用page_key做唯一键,这样「关于我们」这种单页内容可以通过 key 直接查,不用为每个单页建一张表。
2.4 数据库连接工具类的写法
数据库连接是 JSP 项目最容易翻车的地方,很多同学在每个 Servlet 里都写一遍DriverManager.getConnection,结果连接泄漏、代码重复。正确做法是抽一个DBUtil工具类,统一管理连接获取和释放。
package com.company.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/enterprise_site?useSSL=false&serverTimezone=UTC&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明:静态代码块在类加载时执行一次,注册驱动,避免每次获取连接都反射加载。close方法按 ResultSet、Statement、Connection 的顺序关闭,这是 JDBC 资源释放的标准顺序,反过来关会报错。参数上,serverTimezone=UTC是 MySQL 8 驱动必须带的,不带会抛时区异常;characterEncoding=utf8保证中文不乱码。注意your_password要换成你自己的数据库密码,这个类不要提交到公开仓库。
3. 核心功能实现:从首页展示到后台管理的完整链路
3.1 首页数据加载:Servlet 查数据,JSP 只负责渲染
首页通常要展示最新新闻、推荐产品、公司简介三块内容。正确做法是写一个IndexServlet,在doGet里查好数据塞进 request 域,然后转发到index.jsp。
package com.company.servlet; import com.company.dao.NewsDao; import com.company.dao.ProductDao; import com.company.model.News; import com.company.model.Product; 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("/index") public class IndexServlet extends HttpServlet { private NewsDao newsDao = new NewsDao(); private ProductDao productDao = new ProductDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<News> latestNews = newsDao.findLatest(5); List<Product> hotProducts = productDao.findByCategory("推荐", 6); req.setAttribute("latestNews", latestNews); req.setAttribute("hotProducts", hotProducts); req.getRequestDispatcher("/index.jsp").forward(req, resp); } }逻辑说明:@WebServlet("/index")是 Servlet 3.0 的注解方式,不用在web.xml里再配一遍,这是现在的主流写法。findLatest(5)和findByCategory("推荐", 6)是 DAO 层方法,分别取最新 5 条新闻和 6 个推荐产品。req.setAttribute把数据放进请求域,forward转发到 JSP,注意这里必须用 forward 而不是 redirect,因为 redirect 会丢失 request 域的数据。
对应的index.jsp里用 JSTL 标签遍历,不要用 Java 脚本片段:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head><title>企业宣传网站</title></head> <body> <div class="news-list"> <c:forEach items="${latestNews}" var="news"> <div class="news-item"> <h3><a href="newsDetail?id=${news.id}">${news.title}</a></h3> <span class="time"> <fmt:formatDate value="${news.publishTime}" pattern="yyyy-MM-dd"/> </span> </div> </c:forEach> </div> </body> </html>参数说明:<c:forEach>的items对应 request 域里的latestNews,var是循环变量名。<fmt:formatDate>用来格式化日期,pattern里yyyy-MM-dd是年月日,注意MM必须大写,小写mm是分钟,这是新手最常犯的错。用 JSTL 而不是<% %>脚本片段,是因为 JSTL 更清晰,也符合 MVC 里 JSP 只做展示的原则。
3.2 新闻列表分页:三个参数决定翻页是否正常
新闻列表页数据量大,必须分页。分页的核心是三个参数:当前页码pageNo、每页条数pageSize、总记录数totalCount。DAO 层用LIMIT实现。
public List<News> findByPage(int pageNo, int pageSize) { List<News> list = new ArrayList<>(); String sql = "SELECT * FROM news ORDER BY publish_time DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { News n = new News(); n.setId(rs.getInt("id")); n.setTitle(rs.getString("title")); n.setPublishTime(rs.getTimestamp("publishTime")); list.add(n); } } catch (SQLException e) { e.printStackTrace(); } return list; }逻辑说明:LIMIT的第一个参数是偏移量,计算公式是(pageNo - 1) * pageSize,第二个参数是每页条数。比如第 2 页、每页 10 条,偏移量就是 10,取第 11 到 20 条。这里用PreparedStatement而不是字符串拼接,是为了防 SQL 注入,答辩时这一点一定要能说出来。try-with-resources语法会自动关闭 Connection 和 PreparedStatement,比手动 close 更安全。
分页还需要一个查总数的 SQL:SELECT COUNT(*) FROM news,返回totalCount,然后总页数totalPage = (totalCount + pageSize - 1) / pageSize。这个向上取整公式要记住,用totalCount / pageSize直接除会少一页。
3.3 后台登录与权限拦截:Filter 是绕不过去的一环
后台管理页面不能让任何人直接访问,必须做登录校验。做法是写一个LoginFilter,拦截/admin/*路径,检查 session 里有没有登录标记。
@WebFilter("/admin/*") 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; HttpSession session = request.getSession(); Object admin = session.getAttribute("admin"); if (admin == null) { response.sendRedirect(request.getContextPath() + "/admin/login.jsp"); return; } chain.doFilter(req, resp); } }逻辑说明:@WebFilter("/admin/*")拦截 admin 目录下所有请求。session.getAttribute("admin")取登录时存进去的管理员对象,为空说明没登录,重定向到登录页。注意request.getContextPath()会返回项目部署路径,比如/EnterpriseSite,拼上/admin/login.jsp才是完整路径,直接写/admin/login.jsp在非根路径部署时会 404。登录成功后要session.setAttribute("admin", adminObj),退出时session.invalidate()。
3.4 留言板功能:表单提交与防重复提交
留言板是企业和访客互动的入口,实现上要注意两点:一是表单提交后要防止刷新重复插入,二是留言内容要过滤特殊字符。
@WebServlet("/message/add") public class MessageAddServlet extends HttpServlet { private MessageDao messageDao = new MessageDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String name = req.getParameter("name"); String email = req.getParameter("email"); String content = req.getParameter("content"); if (name == null || name.trim().isEmpty() || content == null || content.trim().isEmpty()) { req.setAttribute("error", "姓名和留言内容不能为空"); req.getRequestDispatcher("/message.jsp").forward(req, resp); return; } Message msg = new Message(); msg.setName(name.trim()); msg.setEmail(email); msg.setContent(content.trim()); messageDao.insert(msg); resp.sendRedirect(req.getContextPath() + "/message?success=1"); } }逻辑说明:req.setCharacterEncoding("UTF-8")必须在取参数之前调用,否则中文乱码。参数校验先做,为空就转发回表单页并带错误提示。插入成功后用sendRedirect重定向,这是防重复提交的关键——重定向后浏览器地址栏变成列表页,用户刷新也不会重复提交表单。如果这里用 forward,刷新就会重复插入,这是血泪经验。
4. 部署与打包:传统 JSP 项目怎么打成 war 跑起来
4.1 用 Maven 打包 war 的标准配置
传统 JSP 项目打包成 war 是部署的标准动作。用 Maven 的话,pom.xml里关键配置如下:
<packaging>war</packaging> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>参数说明:<packaging>war</packaging>声明打包类型是 war。servlet-api 和 jsp-api 的scope必须是provided,因为 Tomcat 容器里已经有这两个包了,打进去会冲突。jstl 和 mysql 驱动必须是compile默认作用域,要打进 war 的WEB-INF/lib下。执行mvn clean package后,target目录下会生成EnterpriseSite.war。
4.2 部署到 Tomcat 的两种方式
第一种是直接把 war 包丢进 Tomcat 的webapps目录,启动 Tomcat 会自动解压部署,访问路径是http://localhost:8080/EnterpriseSite/。第二种是在 Tomcat 的conf/server.xml里配 Context,指定docBase指向你的项目目录,适合开发阶段热部署。
# 方式一:拷贝 war 到 webapps cp target/EnterpriseSite.war $CATALINA_HOME/webapps/ # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # Linux/Mac $CATALINA_HOME\bin\startup.bat # Windows注意:如果访问 404,先看 Tomcat 的logs/catalina.out有没有报错,最常见的是数据库连不上导致 Servlet 初始化失败。如果访问出现中文乱码,检查 JSP 页面头部的pageEncoding和数据库连接串的characterEncoding是否都是 UTF-8。
4.3 数据库初始化与配置分离
部署到新环境时,数据库连接信息不能写死在代码里。常见做法是把jdbc.properties放在src下,用类加载器读取。
jdbc.url=jdbc:mysql://localhost:3306/enterprise_site?useSSL=false&serverTimezone=UTC&characterEncoding=utf8 jdbc.username=root jdbc.password=your_password jdbc.driver=com.mysql.cj.jdbc.Driverpublic class DBUtil { private static Properties props = new Properties(); static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("jdbc.properties")) { props.load(in); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new RuntimeException("数据库配置加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password")); } }逻辑说明:getResourceAsStream("jdbc.properties")从 classpath 根目录读取配置文件,Maven 项目里src/main/resources下的文件会被复制到 classpath。这样换环境只改配置文件,不用重新编译代码。注意配置文件里的密码不要提交到公开仓库,答辩演示时用本地库就行。
5. 避坑与排查:答辩前必须填掉的五个坑
5.1 中文乱码:三个地方都要设 UTF-8
现象:页面显示问号或乱码,表单提交的中文存进数据库变成乱码。
原因:JSP 页面编码、请求编码、数据库连接编码三者不一致。JSP 默认可能是 ISO-8859-1,请求默认也是,数据库表可能是 latin1。
解决:JSP 头部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;Servlet 里doPost第一行写req.setCharacterEncoding("UTF-8");数据库连接串加characterEncoding=utf8;建表时指定DEFAULT CHARSET=utf8mb4。四个地方全对上,乱码基本消失。
5.2 404 错误:路径和部署名对不上
现象:访问某个 Servlet 或 JSP 报 404。
原因:一是@WebServlet的路径和访问路径不一致,二是重定向时没加getContextPath(),三是 war 包没部署成功。
解决:先看 Tomcat 的webapps目录下有没有解压出项目文件夹;再看访问路径是否带了项目名,比如/EnterpriseSite/news而不是/news;重定向统一用request.getContextPath() + "/xxx"。如果用了web.xml配 Servlet,检查<url-pattern>和注解是否重复冲突。
5.3 数据库连接失败:驱动和时区是重灾区
现象:启动报ClassNotFoundException或The server time zone value is unrecognized。
原因:MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver;连接串没带serverTimezone。
解决:驱动类名改成com.mysql.cj.jdbc.Driver,连接串加serverTimezone=UTC。如果还是连不上,检查 MySQL 服务是否启动、端口是否 3306、用户名密码是否正确、mysql-connector-java的 jar 是否在WEB-INF/lib下。
5.4 JSTL 标签不解析:jar 包没打进 war
现象:JSP 页面直接把<c:forEach>当文本输出,或者报The absolute uri cannot be resolved。
原因:jstl 的 jar 没在 classpath 里,或者 scope 写成了 provided。
解决:确认pom.xml里 jstl 依赖的 scope 是默认的 compile,打包后检查 war 里的WEB-INF/lib下有没有jstl-1.2.jar和standard-1.1.2.jar。如果是非 Maven 项目,手动把这两个 jar 拷到WEB-INF/lib下。
5.5 后台越权访问:Filter 路径配错等于没拦
现象:没登录也能直接访问/admin/newsList看到后台数据。
原因:Filter 的urlPatterns写成了/admin而不是/admin/*,只拦了 admin 根路径,子路径没拦住。
解决:@WebFilter("/admin/*")星号不能少。另外登录页本身要排除在拦截之外,否则会死循环重定向。可以在 Filter 里判断请求路径,如果是login.jsp或login就放行。答辩时老师很可能会手动输 URL 测试越权,这个坑一定要提前堵上。
6. 让答辩加分的一个技巧:把静态页面改成可配置的单页内容
大部分同学做企业宣传网站,「关于我们」「联系方式」这些页面都是写死的 HTML,改个电话号还得改代码重新部署。我在实际项目里会多做一步:把这些单页内容也存进数据库,后台可编辑,前台动态读取。这个改动不大,但答辩时是个明显的加分项,因为它体现了「内容可维护」的产品思维。
具体做法是复用第 2 章建的page_content表,写一个PageContentServlet,根据pageKey查内容,转发到统一的page.jsp渲染。
@WebServlet("/page") public class PageContentServlet extends HttpServlet { private PageContentDao pageDao = new PageContentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String key = req.getParameter("key"); if (key == null || key.trim().isEmpty()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } PageContent pc = pageDao.findByKey(key); if (pc == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } req.setAttribute("page", pc); req.getRequestDispatcher("/page.jsp").forward(req, resp); } }逻辑说明:前台访问/page?key=about就能看到「关于我们」,访问/page?key=contact看到联系方式。page.jsp里用${page.content}输出,注意如果内容里含 HTML 标签,要用<c:out value="${page.content}" escapeXml="false"/>才能渲染,默认escapeXml="true"会把标签转义成文本。后台再加一个编辑页,管理员改完update一下,前台立刻生效,不用重启 Tomcat。
对应的后台编辑表单核心代码:
<form action="pageUpdate" method="post"> <input type="hidden" name="pageKey" value="${page.pageKey}"/> <input type="text" name="pageTitle" value="${page.pageTitle}"/> <textarea name="content" rows="15">${page.content}</textarea> <button type="submit">保存</button> </form>参数说明:pageKey用隐藏域传,保证更新的是同一条记录;textarea里直接放${page.content},注意不要有多余空格,否则每次保存都会多出空白。更新 SQL 用UPDATE page_content SET page_title=?, content=? WHERE page_key=?。
这个技巧的价值在于,它把「静态展示」升级成了「动态可维护」,答辩时你可以说「企业宣传网站的核心诉求是内容更新及时,所以我把单页内容也做成了后台可配置」。这句话一说,老师就知道你不是照着模板抄的,而是想过业务。
我自己做这类项目最大的教训是:不要一上来就追求功能多,先把首页、列表、详情、后台登录这四条主链路跑通,再考虑留言板、单页配置这些加分项。很多同学卡在环境配置和乱码上耗掉一周,最后功能没做完,答辩时演示都跑不起来。先把最小可运行版本跑起来,再迭代,这个习惯比任何技术细节都重要。希望帮到你。
本文还有配套的精品资源,点击获取