做JSP毕设和课设这几年房产管理系统一直是Java Web方向出现频率最高的题目之一。这个JSP智能房产信息管理系统的设计与实现从完整度上看属于非常标准的课程设计/毕业设计形态程序源码、数据库脚本、调试部署流程、开发环境配置全套齐整。要说这类项目难不难我的真实感受是——功能跑通不难但把它做规范、做扎实能在答辩或演示时经得起追问就需要把很多细节想清楚。这篇博文就按我从拿到项目到部署上线的完整思路来拆一遍包括表结构怎么定、DAO层怎么组织、连接池为什么要配、Tomcat里路径为什么容易404每一处我都会讲清楚背后的原因和踩过的坑。1. 项目定位与整体设计思路1.1 房产信息管理系统的核心应用场景房产信息管理系统说到底就是一个典型的CRUD业务系统围绕房源这个核心实体向外延伸出用户管理、房源发布、房源浏览、预约看房、信息收藏、公告管理等若干条业务线。它解决的是传统纸质台账和Excel表格管理房产信息时存在的几类痛点房源信息更新不及时、查询效率低、多用户同时操作时数据容易乱、权限无法区分。放到实际场景里无论是小型房产中介的内部管理还是学校课程设计里的模拟项目底层需求都是一样的——让不同角色通过Web页面完成房产数据的增删改查并保证数据的集中存储和权限隔离。所以拿到这类项目时第一件事不是急着写代码而是先把角色和业务流程划清楚。以我之前做的一个版本为例我把用户分成三类管理员、经纪人房源发布者、普通访客/注册用户。管理员审核房源、管理用户、发公告经纪人录入和维护自己名下的房源普通用户浏览房源、收藏、发起看房预约。这样一个基础的角色划分直接决定了后面的权限拦截逻辑、表结构设计和页面功能布局。1.2 技术选型为什么绕不开JSP这套组合这个项目标题里明确写的是JSP那么技术上基本就是经典的JSP Servlet JavaBean JDBC MySQL这套组合。很多人会问现在企业里都Spring Boot了JSP是不是过时了这话有一定道理但放在教学、毕设和中小型传统项目迁移的场景里JSP技术栈依然有它不可替代的价值。首先是学习路径上的价值。JSP Servlet让你能亲眼看到HTTP请求从浏览器到服务器、再到数据库再返回的完整链路。JSP里可以直接写Java代码片段Servlet里可以处理请求转发和重定向这个过程比Spring Boot里一堆注解封装后的黑盒要直观得多。其次是部署环境的兼容性。很多学校的实验机房、老旧的服务器环境Tomcat 8 JDK 8 MySQL 5.7这套组合跑得最稳而传统JSP项目打包成WAR包扔进Tomcat webapps目录就能跑的特性在新旧环境迁移时优势非常明显。当然选这套组合也有需要注意的地方。JSP页面如果写太多Java脚本片段Scriptlet维护起来非常痛苦。所以我在做这个项目时从设计层面就把JSP的职责严格限定在展示上——页面里尽量用EL表达式和JSTL标签所有业务逻辑都下沉到Servlet和Service层JSP页面只负责接收request里的数据并渲染。这样既保留了JSP的直观性又不至于让页面变成一团乱麻。2. 数据库设计整个系统的地基2.1 核心表结构拆解数据库是这类项目里最不能糊弄的部分。我见过不少同学把全部功能堆到一两张表里后面改需求的时候痛苦不堪。房产信息管理系统最少需要这么几张核心表第一张是用户表user。字段至少包含用户ID、用户名、密码、真实姓名、手机号、角色管理员/经纪人/普通用户、注册时间。密码字段我强烈建议存MD5加密后的值哪怕这个项目只是课程设计养成这个习惯只有好处没有坏处。第二张是房源表house。这是全系统的核心字段要覆盖房源ID、标题、户型几室几厅、面积、楼层、朝向、装修情况、租金或售价、所在小区、区域位置、详细描述、房源图片路径、所属经纪人ID、审核状态待审核/已通过/已下架、业务类型出售/出租、发布时间。第三张是预约看房表appointment记录哪个用户预约了哪套房源、预约时间、预约状态。第四张是收藏表favorite做用户收藏房源的多对多关系。第五张是公告表notice放管理员发布的站内公告。这里有一个容易被忽略的点房源表和用户表之间的关联。房源的所属经纪人ID要建外键索引查询某个经纪人名下所有房源这类高频操作全靠它走索引。预约表、收藏表则要处理好是逻辑删除还是物理删除——我的建议是预约记录保留逻辑状态字段就行用户在后台看到已取消已完成这类状态比直接删掉记录更符合业务直觉。2.2 建表脚本与初始化数据建表脚本是整个项目源码里最值得仔细读的部分。实际交付的SQL文件通常会包含建库语句、建表语句和初始化数据三部分。我在项目里用的数据库名是house_manager完整的建库建表SQL大致长这样CREATE DATABASE IF NOT EXISTS house_manager DEFAULT CHARACTER SET utf8mb4; USE house_manager; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role TINYINT NOT NULL DEFAULT 2 COMMENT 0-管理员 1-经纪人 2-普通用户, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE house ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, house_type VARCHAR(30) COMMENT 户型如三室两厅, area DECIMAL(8,2), floor_info VARCHAR(30), orientation VARCHAR(10), decoration VARCHAR(30), price DECIMAL(12,2), region VARCHAR(50), address VARCHAR(200), description TEXT, image_path VARCHAR(255), agent_id INT NOT NULL, biz_type TINYINT DEFAULT 1 COMMENT 1-出售 2-出租, status TINYINT DEFAULT 0 COMMENT 0-待审核 1-已上架 2-已下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_agent (agent_id), KEY idx_status (status), CONSTRAINT fk_house_agent FOREIGN KEY (agent_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有几个细节要重点说明。第一字符集一定要用utf8mb4而不是utf8否则用户输入生僻字或emoji表情时会出现乱码甚至直接报错。第二DECIMAL而不是FLOAT来存金额FLOAT有精度丢失问题做价格区间查询时会出幺蛾子。第三所有表的存储引擎都用InnoDB它支持外键约束和事务对于预约、收藏这类需要一致性保证的操作非常关键。第四初始化数据是程序能跑起来的必要条件——至少要有一个管理员账号否则系统的后台入口就是死的。通常初始化脚本里会先插入admin/admin123这类账号再把默认密码的MD5值写好。2.3 连接池配置为什么不能每次new Connection数据库连接这块很多源码里会看到一个DBUtil工具类里面通过Class.forName加载驱动、DriverManager.getConnection获取连接。这种写法在演示项目里能跑但性能上有个很大的问题每次请求都新建一个物理连接用完再断开。MySQL的默认连接数上限是151一旦并发稍高系统就报Too many connections。我实际做的时候直接用Druid连接池替代了原生JDBC获取连接的方式。配置一个druid.properties文件放在src目录下driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/house_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 minIdle5 maxWait60000然后在DBUtil里通过DruidDataSourceFactory创建一个DataSource实例之后每次要连接就从池里borrow用完归还而不是关闭。这个改动对并发能力的提升非常明显。顺带说一句url参数里的serverTimezoneAsia/Shanghai特别重要MySQL 8.x版本不指定时区会直接报时区错误这个坑很多第一次部署的同学都会踩到。3. 核心代码实现与关键细节3.1 JSP页面层的设计技巧JSP页面写得好不好直接决定这个项目看起来像不像一个正经系统。我的原则是JSP里不允许出现大段Java代码逻辑处理全部放到Servlet里完成JSP只负责从request和session中取数据、做循环渲染。要实现这一点必须引入JSTL标签库。在页面头部引入标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %然后列表页渲染就非常清爽了c:forEach items${houseList} varhouse div classhouse-card h3${house.title}/h3 p户型${house.houseType}面积${house.area}㎡/p p价格fmt:formatNumber value${house.price} pattern#,##0/元/p a hrefhouse/detail?id${house.id}查看详情/a /div /c:forEach这里有一个很多人会忽略的细节JSP页面里取数据用的属性名必须和Servlet里setAttribute的名字完全一致。比如Servlet里写的是request.setAttribute(houseList, list)那JSP里就是${houseList}。如果名字对不上页面不会报错只会显示空白这是新手最常遇到的怎么我页面没数据问题的根本原因。公共头尾部分用include指令复用。每个页面都去写一遍导航栏和页脚纯属浪费我在header.jsp和footer.jsp里把公共部分抽出来需要的地方用% include fileheader.jsp %引入。注意include有两种方式静态include和动态include对于公共导航这种内容用静态include也就是上面的写法就够了。3.2 Servlet控制器与JDBC数据访问Servlet层的作用是接收请求、调用业务逻辑、决定跳转到哪个页面。这里我用BaseServlet做一个统一入口的设计所有的业务Servlet都继承BaseServlet通过一个action参数来路由方法。这样就不需要写一堆类一个HouseServlet里就能容纳列表、详情、新增、修改、删除等多个方法。WebServlet(/house/*) public class HouseServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); if (/list.equals(path)) { list(req, resp); } else if (/detail.equals(path)) { detail(req, resp); } else if (/add.equals(path)) { add(req, resp); } // 其他路由 } }数据访问层我分成DAO接口和DAO实现类两层。HouseDao接口定义方法签名HouseDaoImpl里用JDBC完成具体的SQL执行。查询操作使用PreparedStatement预编译一方面防止SQL注入另一方面执行效率比Statement高。public ListHouse findByCondition(String region, Double minPrice, Double maxPrice) { StringBuilder sql new StringBuilder(SELECT * FROM house WHERE status1 ); ListObject params new ArrayList(); if (region ! null !region.isEmpty()) { sql.append(AND region ? ); params.add(region); } if (minPrice ! null) { sql.append(AND price ? ); params.add(minPrice); } if (maxPrice ! null) { sql.append(AND price ? ); params.add(maxPrice); } sql.append(ORDER BY create_time DESC); // 执行查询并封装结果 }这条查询会用于列表页的多条件筛选所以SQL是动态拼接的。动态SQL最怕的就是条件拼接出错多一个空格、少一个空格都可能导致语法错误。我的经验是先在Navicat里把SQL跑通再粘到代码里别直接在代码里试。另外PreparedStatement的参数下标从1开始params列表里第几个参数就对应SQL里第几个问号顺序错一位就会报参数不匹配或取到错误结果。从ResultSet到对象的封装我手动写了一个RowMapper式的转换方法每一列通过rs.getXxx(字段名)取值。这里列名的大小写不敏感但字段名必须和数据库里真实存在的列名一致。数据库里叫house_typeJavaBean里叫houseType映射时就要注意rs.getString(house_type)这个算是JSP项目里高频低级错误了。3.3 登录权限与安全拦截权限控制是这类管理系统里最见功力的一部分。不登录就能直接访问管理员页面这在答辩时基本属于送命题。我在项目里通过一个LoginFilter过滤器来解决这个问题WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); User loginUser (session ! null) ? (User) session.getAttribute(loginUser) : null; String uri req.getRequestURI(); // 放行登录、注册接口和静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(register.jsp) || uri.contains(/user/login) || uri.contains(/user/register) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(request, response); return; } // 未登录跳转登录页 if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } // 管理员路径校验角色 if (uri.contains(/admin/) loginUser.getRole() ! 0) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }这个Filter有两个细节值得说明。第一req.getSession(false)里的false很关键它表示如果当前没有session就返回null而不是新建一个。如果写成getSession()那每个请求都会强制创建一个session没登录的用户也会有一堆session存留在服务器上。第二放行规则里必须包含静态资源路径。CSS、JS、图片这些资源没有登录态不放行的话页面会样式全丢只剩一堆裸HTML。密码安全方面我用MD5加盐的方式做了处理。用户注册时把用户名作为盐拼到密码后面再取MD5这样即使两个用户密码相同存出来的密文也不一样。虽然MD5在今天看来不算强加密但作为课程设计项目已经比明文存储好了几个量级。4. 开发环境搭建与项目导入全流程4.1 环境版本搭配建议这个项目的开发环境讲究一个版本匹配不是越新越好。我推荐一套经过大量项目验证的稳定组合组件推荐版本说明JDK1.8兼容性最强绝大多数Tomcat版本都支持开发IDEIntelliJ IDEA 2023社区版够用Ultimate对Java Web更友好应用服务器Tomcat 8.5 / 9.0和JDK 8搭配非常稳数据库MySQL 5.7 / 8.05.7兼容性更好8.0需注意时区和驱动版本连接驱动mysql-connector-java 5.1.49 / 8.0.x版本必须和MySQL版本匹配这里特别说一下MySQL 8.0和驱动的匹配问题。如果用MySQL 8.0驱动类名要写成com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver同时JDBC的url里必须带serverTimezoneAsia/Shanghai。很多同学把项目从5.7环境挪到8.0环境后报ClassNotFoundException或者时区错误基本都是这两个地方没改。4.2 从源码导入到跑通项目的完整步骤拿到源码包之后整个导入和启动流程是有固定套路的。第一步用IDEA新建一个空项目或直接Open源码目录选择Maven或普通Web项目结构。如果是带pom.xml的Maven项目IDEA会自动下载依赖如果是传统lib目录方式需要手动把lib下的jar包Configure到Project Structure里。第二步配置Tomcat。IDEA里打开Run Configuration新增Tomcat Server - Local指定Tomcat安装路径。Deployment选项卡里把项目的Artifact以war exploded方式添加这种方式支持热部署修改JSP后不用重启Tomcat直接刷新浏览器就能看到效果。Application context建议设置为/house这样所有访问路径都是以http://localhost:8080/house开头。第三步初始化数据库。用Navicat或命令行执行项目里提供的SQL脚本然后修改DBUtil或druid.properties里的数据库用户名和密码。这一步是跑不通问题里出现频率最高的原因十个报数据库连接错误的九个是没改密码就直接启动。第四步启动项目。点击Tomcat的Debug模式启动控制台出现信息: Server startup in [xxx] milliseconds就说明启动成功了。然后浏览器访问http://localhost:8080/house/login.jsp用初始化数据里的管理员账号登录整个流程就完整跑通了。4.3 JSP项目打包部署的两种方式除了在IDEA里开发调试很多场景需要把项目打成一个WAR包部署到独立的Tomcat里。传统JSP项目打包WAR有两种方式。第一种是IDEA里的Build - Build Artifacts选择Web Application: Archive构建完成后在out目录下找到WAR文件。第二种是Maven项目直接执行mvn clean packagetarget目录下会生成同名WAR包。拿到WAR包后部署就很简单了把WAR文件复制到Tomcat安装目录下的webapps文件夹里然后启动Tomcat。Tomcat会自动解压WAR包并部署。这里注意WAR包的名字就是应用的访问路径比如house.war解压后访问路径就是http://localhost:8080/house。如果想改访问路径直接改WAR包的文件名即可。部署之后有一件事很多新手会忽略检查WAR包里的配置文件有没有被打进去。有时候druid.properties放在src目录下但没有被Maven识别为资源文件构建出来的WAR包里根本找不到这个文件启动时就会报无法初始化连接池。解决办法是在pom.xml里显式声明resources目录或者直接把properties文件放到src/main/resources下。5. 常见问题排查与避坑指南5.1 典型报错速查表我把JSP项目里最常见的报错和排查思路整理成了一张表按出现频率排序报错信息根本原因解决办法404 Not Found访问路径不存在或Servlet未映射检查WebServlet注解路径、web.xml配置、项目Context Path500 空指针异常页面取不到数据或DAO返回null检查setAttribute和getAttribute名字是否一致、数据库里是否有数据Access denied for user数据库用户名或密码错误重新核对druid.properties或DBUtil里的连接参数Unknown database数据库不存在执行SQL脚本中的CREATE DATABASE语句ClassNotFoundException: com.mysql.jdbc.Driver驱动jar缺失把mysql-connector jar添加到WEB-INF/lib目录中文乱码编码不一致统一页面、Servlet、数据库连接三处编码为UTF-8Too many connections连接未释放改用连接池用完连接必须关闭无法启动Tomcat端口被占用1099或8080端口已有程序使用netstat -ano5.2 中文乱码这个老大难问题中文乱码在JSP项目里几乎人人都会遇到而且它可能出现在三个不同的环节页面显示乱码、数据库存储乱码、URL参数传递乱码。三个环节的原因各不相同排查方法也不一样。页面显示乱码检查JSP文件头部的page指令是否写了% page contentTypetext/html;charsetUTF-8 %同时确保文件本身是以UTF-8编码保存的。IDEA右下角能看到文件编码如果显示GBK右键File Encoding改成UTF-8并Reload。数据库存储乱码检查三处数据库连接的url里有没有characterEncodingutf8、MySQL的my.ini里character-set-server是否设为utf8mb4、建表语句里有没有DEFAULT CHARSETutf8mb4。前面任何一处掉链子页面输入中文存到库里就是问号。URL参数乱码常见于通过GET请求传递中文参数比如进行关键字搜索时。解决方式是在Tomcat的server.xml里的Connector上增加URIEncodingUTF-8属性有的版本还需要加useBodyEncodingForURItrue。这两个属性加上之后GET请求里的中文参数基本就不会乱码了。5.3 数据提交表单时的几个隐蔽坑做新增房源或修改房源功能时表单提交有几个隐蔽问题。第一个问题是日期类型的数据JSP里如果有预约时间表单控件通常给的是字符串直接用String接收然后往数据库里存会报错。我的处理方式是在Servlet里用SimpleDateFormat做一次转换SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm); Date appointmentTime sdf.parse(req.getParameter(appointmentTime));解析失败时记得用try-catch包住不能让它抛异常到页面。第二个问题是复选框的取值比如多选房源的配套设施时表单里多个同名参数用req.getParameterValues(facility)拿数组不能只用getParameter否则只能取到第一个值。第三个问题也是我见别人踩得最多的在JSP页面里做新增操作时Form表单的action写成绝对路径。比如action/house/add如果项目context path不是根路径这个请求会直接404。正确写法是action${pageContext.request.contextPath}/house/add它是动态获取当前项目的上下文路径不管部署到哪里都不会出错。5.4 SQL注入课程设计也要守住的底线有些JSP项目为了省事直接把用户输入的参数拼到SQL字符串里执行。这在功能上是能跑通的但安全性几乎为零。比如登录页如果写成String sql SELECT * FROM user WHERE username username AND password password ;用户在用户名输入框里输入 OR 11密码随便填这个SQL条件永远成立系统就能被直接登录进去。这就是最典型的SQL注入攻击。由于项目的性质和场景我在这里不做攻击演示只强调防御核心就是所有SQL都改成PreparedStatement的预编译参数绑定方式让用户输入的内容永远作为参数值而不是SQL片段。同时在DAO层做好字符串的转义和类型转换规范上不留下任何裸拼SQL的口子。这个习惯无论项目大小都应该保持住。6. 从演示项目到真正能用的系统几步锦上添花的改造如果基础功能都跑通了时间还富余我建议做几个高性价比的升级这些内容在答辩或项目展示时都很加分。第一个升级是给房源列表加上分页。现在大多数源码里的列表是直接查全部数据一多页面就非常长。分页的实现思路不复杂页面传两个参数currentPage和pageSizeSQL里用LIMIT做偏移计算同时查一个count总数算出总页数。前台分页组件用JSTL循环渲染页码高亮当前页。顺手把每页显示的条数也做成可配置的。第二个升级是做多条件组合查询。按区域、户型、价格区间、业务类型进行筛选用JDBC动态拼接SQL。这个功能既实用又能在答辩时展示你对动态SQL的掌握程度。第三个升级是图片上传。房源信息里图片路径字段目前多半是手动填的一个URL如果能做成从本地上传图片到服务器指定目录并把相对路径存到数据库系统的真实感会提升非常多。用Apache Commons FileUpload组件10年历史的老库但在JSP项目里依然稳定可靠。第四个升级是操作日志和统计图表。记录用户的关键操作比如管理员下架房源、用户发起预约存一张log表做一个后台统计页显示总房源数、本周新增、热门区域排行等数据。统计这块用简单的SQL聚合加JSP表格展示就够不需要引入复杂的前端图表库。写到这里这个项目的核心脉络已经全部走了一遍。我个人做这类JSP项目的实际体会是不要因为技术栈老就轻视它恰恰是它足够简单直接才能让你把注意力放在请求-处理-响应这条主链路的理解上。你要想在答辩中拿到高分比起功能清单面试官和老师更看重的是你对为什么要这样设计的回答——为什么用连接池、为什么用PreparedStatement、为什么加过滤器做权限控制这些我在上文中提到的细节都是值得展开讲一讲的关键点。最后分享一个小经验拿到任何一套JSP源码无论功能多复杂先做三件事——通读数据库脚本理解表关系找到DBUtil或配置文件确认连接参数再看BaseServlet的路由设计。这三件事做完整个项目的骨架就基本掌握在你手里了剩下的都是在骨架上填充肌肉和细节。只要按照这篇文章的流程和思路来从导入源码到完成调试部署一般只需要半天时间就能看到一个运行顺畅的房产信息管理系统。