
简介这套问卷调查系统基于SpringMVC、Spring和MyBatis整合的SSM架构前端采用JSP、jQuery与layui组件面向完成Java Web课程设计、毕业设计或希望理解SSM后台开发全流程的读者。管理端提供管理员信息增删改查、问卷制作、问卷发布、结果统计与报表展示用户端支持在线填写问卷整体功能覆盖从建卷、发卷到回收统计的完整闭环代码结构按Controller、Service、Dao分层便于二次扩展。压缩包以rar格式打包共847个文件、42.05MB主要包含Java源码、JSP视图页面、XML配置与MyBatis映射、JS/CSS/layui静态资源、数据库SQL脚本以及可直接运行的war包其中class与jar文件也方便对照源码调试。项目基于JDK 1.8与Tomcat 7-9构建数据库使用MySQL 5.7适合在IDEA/Eclipse等开发环境中导入调试。资源上线以来已有1226人学习适合需要对照真实项目梳理SSM整合要点、学习layui后台界面搭建或将问卷业务模块迁移到自身项目的开发者。1. 这套「SSM JSP layui」问卷系统到底值不值得你搭一遍如果你打开这个标题大概率是正在做课程设计、毕业设计或者公司内部急需一套轻量匿名问卷工具。先说结论SSMSpring SpringMVC MyBatis加 JSP 加 layui 加 MySQL 这套组合放在今天确实不算新潮但它恰好卡在「能跑、能改、能交差」和「不需要折腾前端工程化」的舒适区里。Java 后端逻辑由 Spring 管理对象、SpringMVC 管路由、MyBatis 管 SQL页面用 JSP 直接在服务端渲染UI 用 layui 的组件库把表单、弹层、分页做齐数据落 MySQL。这套方案的适用人群非常明确你手上有 Java 基础、想要一个能完整演示「建问卷、发问卷、填问卷、看统计」闭环的项目又不想引入 Vue 或 React 重写前后端分离架构。它的价值不在于技术新颖而在于所有环节都能在本地被完整复现。接下来的内容会按「表结构设计 → 环境搭建 → 核心功能实现 → 踩坑记录 → 优化验证」的顺序展开每一步都会给出能直接复制的代码和参数说明。2. 先把架构拆清楚SSM、JSP、layui 在这个系统里各唱什么角2.1 SSM 的天然分工谁管对象、谁管路由、谁管数据库Spring 在这个系统里承担的是容器职责。问卷系统里有SurveyService、QuestionService、ResponseService之类的业务对象它们之间的依赖关系如果用手动new来管理改一个构造参数就要牵连一片代码。Spring 的Autowired注解让对象之间的组装变成了配置声明这也是后期维护时最值钱的部分——你新增一个统计模块不需要去改调用方的创建逻辑。SpringMVC 管的则是「浏览器请求打到哪个 Java 方法」这件事。以问卷发布为例管理员在页面上点击「发布问卷」浏览器发出POST /survey/publish请求DispatcherServlet 根据RequestMapping找到对应的 Controller 方法方法执行完后返回survey_list.jsp的逻辑视图名由视图解析器拼接出完整 JSP 路径。这里有一个关键参数值得注意在spring-mvc.xml里配置视图解析器时prefix通常设为/WEB-INF/views/suffix设为.jsp。把 JSP 放在 WEB-INF 下能防止用户通过浏览器直接访问模板文件这是一道最基础的权限边界。MyBatis 解决的是 SQL 和 Java 对象的映射问题。问卷系统典型的操作是查询一张问卷的所有题目、批量插入用户的作答记录、统计某个选项的被选次数。这些操作如果用 JDBC 手写每个方法都要重复获取连接、创建 PreparedStatement、遍历 ResultSet 封装对象。MyBatis 允许把 SQL 写在 Mapper XML 文件里通过resultType或resultMap把查询结果直接映射成Survey、Question这类 POJO对于统计类聚合查询直接返回MapString, Object灵活性比强类型实体更高。2.2 JSP 在系统里不只是模板它还承担了权限判断现在的开发人员可能更习惯前后端分离但 JSP 在这个项目里的地位不是「无奈之选」而是「够用且直接」。JSP 的一个关键特性是可以直接在页面里用c:if、c:forEach标签操作后端传过来的数据也可以通过${sessionScope.user.role}这样的 EL 表达式判断当前登录者的角色。在问卷系统里这个特性被用在两个地方。第一处是页面渲染差异同一个survey_detail.jsp当登录者是管理员时显示「查看统计」按钮当登录者是普通受访者时显示「立即填写」。第二处是基础访问控制在web.xml里配置一个LoginFilter拦截所有/survey/*和/admin/*路径在doFilter里判断session.getAttribute(userId)是否为空。这个过滤器放行登录页和静态资源其他请求一律重定向到login.jsp。这套做法的好处是代码量少、逻辑直观调试时打断点很方便缺点是页面里稍微混入了展示逻辑但这对于中小型内部系统完全可以接受。2.3 layui 负责把后台页面的颜值和服务端渲染对接起来layui 是一个「由服务端渲染页面直接引用的前端 UI 库」不需要 npm 安装、不需要 webpack 打包在 JSP 页面里通过link和script标签直接引入layui.css和layui.js即可。这对 SSM JSP 项目来说是极大的便利省掉了前后端分离带来的跨域和 token 认证问题。在这个问卷系统里layui 主要有三个应用场景。问卷列表页使用layui.table模块但这里不是让前端 Ajax 去拉 JSON而是后端渲染好表格主体layui 只负责分页条的生成和操作列的样式问卷编辑页使用layui.form模块它提供的lay-verifyrequired属性可以直接完成表单非空校验减少自己写 JS 校验的工作量问卷填写页使用layui.laytpl模板引擎用于根据题目的类型单选、多选、填空动态渲染不同的输入控件。这套组合的体验接近传统 Bootstrap 风格但组件丰富度和交互细节更好尤其适合后台管理系统这类对视觉效果要求不高的场景。3. 从建库到跑通最小命令表结构设计和 SSM 项目初始化3.1 五张表撑起整个问卷业务问卷、题目、选项、作答、用户问卷系统的核心业务不复杂但表结构设计如果偷懒后面写统计 SQL 时会痛苦。我建议按以下五张表来建其中逻辑外键关系都建立索引CREATE TABLE survey ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 问卷ID, title VARCHAR(200) NOT NULL COMMENT 问卷标题, description TEXT COMMENT 问卷说明, status TINYINT DEFAULT 0 COMMENT 0-草稿 1-发布中 2-已关闭, created_by INT COMMENT 创建人ID关联sys_user, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, publish_time DATETIME NULL COMMENT 发布时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT问卷主表; CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, survey_id INT NOT NULL COMMENT 所属问卷ID, qtype TINYINT NOT NULL COMMENT 1-单选 2-多选 3-填空, content VARCHAR(500) NOT NULL COMMENT 题干, sort_no INT DEFAULT 0 COMMENT 题目排序号, is_required TINYINT DEFAULT 1 COMMENT 是否必答 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目表; CREATE TABLE question_option ( id INT PRIMARY KEY AUTO_INCREMENT, question_id INT NOT NULL COMMENT 所属题目ID, option_text VARCHAR(200) NOT NULL COMMENT 选项内容, sort_no INT DEFAULT 0 COMMENT 选项排序号 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选项表; CREATE TABLE answer_record ( id INT PRIMARY KEY AUTO_INCREMENT, survey_id INT NOT NULL, question_id INT NOT NULL, answer_type TINYINT COMMENT 1-选项ID 2-文本内容, answer_value VARCHAR(500) COMMENT 选项ID或填空文本, respondent_ip VARCHAR(64) COMMENT 作答者IP用于基础去重, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_survey_question (survey_id, question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT作答明细表; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 推荐存MD5或BCrypt, role TINYINT DEFAULT 0 COMMENT 0-管理员 1-普通用户 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;建表时要注意一个细节answer_record里的answer_value是混合语义的当题型是单选或多选时存的是选项 ID当题型是填空时存的是文本。这种设计在被统计时需要有条件判断但好处是省掉一张宽表或 EAV实体-属性-值模型的复杂度。UNIQUE KEY uk_survey_question是用来防同一用户对同一题重复提交的配合程序层的 Session 校验双保险。3.2 项目目录结构严格按 SSM 的分层约定摆放创建 Maven 项目时建议使用war打包方式因为 JSP 项目最终要部署到 Tomcat 的webapps目录下。一个可复现的目录结构如下src/main/java ├── com.survey.controller # SpringMVC 控制器 ├── com.survey.service # 业务接口 ├── com.survey.service.impl # 业务实现 ├── com.survey.dao # MyBatis Mapper 接口 ├── com.survey.entity # POJO 实体类 ├── com.survey.filter # 登录过滤器 src/main/resources ├── jdbc.properties # 数据库连接配置 ├── mybatis-config.xml # MyBatis 全局配置 ├── spring-context.xml # Spring 根容器配置 ├── spring-mvc.xml # SpringMVC 配置 └── mapper ├── SurveyMapper.xml ├── QuestionMapper.xml └── AnswerRecordMapper.xml src/main/webapp ├── WEB-INF/web.xml ├── WEB-INF/views │ ├── login.jsp │ ├── survey_list.jsp │ └── survey_detail.jsp └── static ├── layui/ # layui 的 css/js 文件 └── css/style.css3.3 三个配置文件缺一不可jdbc.properties、spring-context.xml、spring-mvc.xml数据库连接直接写在 Java 代码里是新手最常见的翻车操作。正确的做法是把连接信息抽到jdbc.properties然后在 Spring 配置里用PropertyPlaceholderConfigurer读取。jdbc.properties的内容如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/survey_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyourpassword注意 URL 里的serverTimezoneAsia/Shanghai参数MySQL 8.x 驱动强制要求时区设置不写会直接报The server time zone value ...异常。useSSLfalse是避免本地连接时 SSL 握手额外开销生产环境按需调整。MyBatis 的整合用mybatis-spring桥接。在spring-context.xml里配置MapperScannerConfigurer扫描com.survey.dao包让它自动生成 Mapper 代理对象bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.survey.dao / property namesqlSessionFactoryBeanName valuesqlSessionFactory / /beansqlSessionFactory则需要注入数据源和 mybatis-config 的位置bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property nameconfigLocation valueclasspath:mybatis-config.xml / /bean3.4 第一个能跑通的最小查询SurveyMapper 的 XML 与调用链写完配置先别急着写复杂功能跑通一条最简单的查询链路来验证环境是最高效的做法。在SurveyMapper.java接口里定义一个方法Repository public interface SurveyMapper { ListSurvey selectAllSurveys(); }对应的SurveyMapper.xml中这样写select idselectAllSurveys resultTypecom.survey.entity.Survey SELECT id, title, description, status, created_time FROM survey ORDER BY created_time DESC /select然后在SurveyService中注入 Mapper在 Controller 中调用并返回列表Service public class SurveyServiceImpl implements SurveyService { Autowired private SurveyMapper surveyMapper; Override public ListSurvey getSurveyList() { return surveyMapper.selectAllSurveys(); } }最后在 Controller 中把数据塞进 ModelAndViewJSP 用${surveyList}遍历即可。运行这个链路时重点验证三件事Tomcat 端口能否正常访问、数据源有没有连上 MySQL、MyBatis 的 resultType 映射是否成功。如果页面 500 报错先去 Tomcat 的logs/localhost.log看堆栈第一行多半是 SQL 语法或 mapper XML 的命名空间问题。4. 核心功能逐块实现问卷发布、作答交互与统计报表4.1 问卷发布知识点在「事务边界」和「批量插入」发布一份问卷的操作涉及两张表survey主表插入一条记录question表插入若干条题目然后根据题型决定是否向question_option插入选项。这三个操作必须包在同一个事务里否则会出现问卷创建成功但题目丢失的脏数据。Controller 接收前端传来的 JSON 字符串然后用一个复合对象接收。在 Service 层用Transactional注解声明事务实现代码逻辑如下Transactional(rollbackFor Exception.class) public Integer createSurvey(Survey survey, ListQuestion questions) { surveyMapper.insertSurvey(survey); for (Question q : questions) { q.setSurveyId(survey.getId()); questionMapper.insertQuestion(q); if (q.getQtype() 1 || q.getQtype() 2) { for (Option opt : q.getOptions()) { opt.setQuestionId(q.getId()); optionMapper.insertOption(opt); } } } return survey.getId(); }这里有一个参数细节Transactional默认只在RuntimeException时才回滚如果业务方法抛出的是SQLException这类受检异常事务不会回滚。所以rollbackFor Exception.class这个属性至关重要。批量插入时如果数据量大可以改成 MyBatis 的foreach批量 insert但小规模问卷系统单次不超过 50 题用循环逐条插即可代码可读性更强。4.2 问卷作答提交用一条 INSERT 记录所有答案受访者填写完问卷后前端会把所有答案以surveyId1questionId3answerValue2questionId4answerValue......的格式提交。建议在 Controller 里用数组参数接收然后循环入库RequestMapping(/survey/submit) ResponseBody public String submit(RequestParam Integer surveyId, RequestParam Integer[] questionIds, RequestParam String[] answerValues) { int count 0; for (int i 0; i questionIds.length; i) { AnswerRecord ar new AnswerRecord(); ar.setSurveyId(surveyId); ar.setQuestionId(questionIds[i]); ar.setAnswerValue(answerValues[i]); ar.setRespondentIp(getClientIp(request)); answerRecordMapper.insert(ar); count; } return count questionIds.length ? success : partial; }注意这里的前端参数名必须与RequestParam里的名称完全一致顺序问题由questionIds和answerValues两个数组的下标对齐来解决。这个设计看起来简单但实际有不少项目在这里栽过原因是用ListAnswerRecord接收时没有设定正确的参数名称前缀导致绑定失败所有答案全部为 null。4.3 统计报表的关键 SQL按选项分组计数和文本答案提取问卷系统的灵魂在统计。对于单选题需要统计每个选项被选了多少次SQL 写法如下SELECT o.id AS option_id, o.option_text, COUNT(a.id) AS answer_count FROM question_option o LEFT JOIN answer_record a ON a.answer_value o.id WHERE o.question_id #{questionId} GROUP BY o.id, o.option_text ORDER BY o.sort_no这里用LEFT JOIN而不是INNER JOIN目的就是让没有被任何人选择的选项也出现在结果里计数为 0。这点在调查类问卷中很重要——如果某个选项无人问津你应该给管理员展示「0 票」而不是直接消失否则会误导判断。对于多选题目由于answer_value存的是单个选项 ID统计时要按同一个问卷同一道题目去重精确写法如下SELECT a.answer_value, COUNT(*) AS cnt FROM answer_record a WHERE a.survey_id #{surveyId} AND a.question_id #{questionId} GROUP BY a.answer_value ORDER BY cnt DESC最后在 JSP 端展示时可以用 layui 的进度条模块把投票数渲染成横向条状视觉上比表格直观得多。5. 避坑指南SSM JSP layui MySQL 的高频翻车现场5.1 IDEA 热部署失效导致 JSP 修改不生效现象改了 JSP 页面刷新浏览器后依然是旧内容有时要重启 Tomcat 才能看到变化。原因IDEA 默认把 Tomcat 的部署模式设置为「使用默认工作目录且不自动更新资源」。JSP 文件不像 Java 类那样能热替换需要额外的配置。解决在 IDEA 的 Run Configuration 中切换到 Deployment 标签页把项目的 Deployment 方式选为war exploded爆炸式部署。然后在 Server 标签页勾选Update classes and resources并把 On frame deactivation 设为Update resources。这样每次 IDEA 窗口失焦时JSP 和静态资源会被自动复制到 Tomcat 的部署目录。5.2 layui 表单组件没有渲染出样式现象layui.use([form], function(){})代码没有报错但页面上的 select、checkbox 仍是浏览器原生样式lay-submit按钮点击后没有拦截。原因layui 的form.render()没有被调用。layui 表单一类的组件在页面加载完成时只有调用form.render()后才会将原始表单项替换为 layui 样式。解决在 JSP 底部 script 段中先定义监听器再调用form.render()layui.use([form, layer], function(){ var form layui.form; form.on(submit(submitBtn), function(data){ // 收集表单数据手动构造AJAX请求提交 return false; // 阻止原生表单提交 }); form.render(); });5.3 MySQL 5.7 与 8.0 驱动的兼容性差异现象本地用的是 MySQL 5.7但 pom.xml 里引入的是mysql-connector-java8.0.x 版本启动时报Public Key Retrieval is not allowed或连接超时。原因8.0 驱动默认使用 caching_sha2_password 认证连接时需要拿服务器的公钥而 5.7 默认使用 mysql_native_password两边握手协议不同。解决不建议换回 5.x 驱动而是在 JDBC URL 上追加两个参数allowPublicKeyRetrievaltrueuseSSLfalse。同时确认 MySQL 用户表里的认证方式是mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY yourpassword; FLUSH PRIVILEGES;5.4 JSP 自定义标签失效导致 EL 表达式被当作纯文本现象页面里${surveyList}显示原样字符串没有解析成 Java 变量值。原因web.xml 的web-app根元素版本声明不对。如果声明为 2.3 或更早版本Servlet 容器处于 JSP 1.2 模式默认不启用 EL 表达式。解决web.xml 头改成 Servlet 3.0 以上版本声明web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.15.5 数据库连接断了但页面不报错只显示超长时间 loading现象MySQL 服务如果中途重启或长时间无访问Tomcat 连接池里的连接变成“死连接”页面请求一直卡住最后报连接池相关的 Timeout 异常。原因连接池没有配置空闲连接检测和回收策略。DBCP 或 C3P0 默认配置对 MySQL 的wait_timeout参数不够敏感。解决如果用的是 Druid在数据源配置里增加property nameminEvictableIdleTimeMillis value60000 / property nametimeBetweenEvictionRunsMillis value30000 / property namevalidationQuery valueSELECT 1 / property nametestWhileIdle valuetrue /这样连接池会每 30 秒运行一次检测剔除空闲超过 60 秒的连接并用SELECT 1验证连接有效性避免从池里拿到已断开的连接。6. 进阶优化与验证技巧把系统从「能跑」推向「能上线」系统功能做完后最先要做的是验证统计结果是否正确。我习惯的做法是写一个批量模拟作答的 Java 测试脚本随机生成 100 份答案提交到接口然后对比数据库里的记录数和统计 SQL 的结果。如果记录数一致且每个选项的投票比例符合随机分布预期说明提交链路和统计链路都没问题。// 模拟作答脚本随机生成100份问卷答案 public static void main(String[] args) { Random r new Random(); HttpClient client new HttpClient(); for (int i 0; i 100; i) { ListNameValuePair params new ArrayList(); params.add(new BasicNameValuePair(surveyId, 1)); for (int qid 1; qid 5; qid) { params.add(new BasicNameValuePair(questionIds, String.valueOf(qid))); params.add(new BasicNameValuePair(answerValues, String.valueOf(r.nextInt(3) 1))); } client.post(http://localhost:8080/survey/submit, params); } }性能层面的优化最值得做的是给answer_record表加一个(survey_id, question_id)复合索引。这个操作对单表统计的查询速度提升非常明显尤其是当答案记录超过十万条时不加索引的GROUP BY很容易触发慢查询日志。另外如果需要展示「同 IP 是否已经作答过」可以基于(survey_id, respondent_ip)再建一个唯一索引比在代码里用SELECT COUNT(*)判断快得多。还有一个我常用的优化习惯是把高频常量查出来放到application作用域例如问卷的status字段。与其每次渲染列表时都查询survey表不如在系统启动时用ApplicationListener加载一份MapInteger, String给页面用 EL 直接取。这个技巧对这个规模的系统收益不大但能帮你理解 Spring 容器启动生命周期。整个系统的验证路径就三步先跑通最小链路确认环境再按「发布 → 填写 → 统计」做端到端功能测试最后用脚本模拟数据做压力验证。这套 SSM JSP layui 的方案做完之后你能理清 Spring 的容器管理、SpringMVC 的请求流转、MyBatis 的 SQL 映射和 JSP 服务端渲染这四条线。按这个思路搭过一次以后再接触 Spring Boot 或前后端分离项目时很多概念会自然迁移过去。这是我做过多个 Java Web 课程项目之后沉淀下来的一套做法希望帮到你。本文还有配套的精品资源点击获取