SSM+Layui+JSP问卷调查系统实战:从建表到部署的完整链路
发布时间:2026/10/2 8:32:45来源:尧图网络
简介这是一套基于SpringMVCSpringMyBatis的SSM问卷调查系统完整项目源码采用JSP、jQuery与layui构建前端界面适合Java Web初学者、课程设计或毕业设计开发者参考学习。系统分为管理端与用户端管理员可完成信息增删改查、制作与发布问卷、统计调查结果并生成报表展示普通用户则可在线填写问卷覆盖问卷全流程业务场景。资源包共847个文件包含300个gif、138个js、44个css、36个jsp、31个java源码及35个jar依赖等涵盖页面素材、样式脚本、业务代码与运行库压缩包约42.05MB并附有sql建库脚本与war包便于快速部署。项目基于Maven构建推荐JDK1.8、Tomcat7至9及MySQL5.7环境运行导入数据库后即可启动体验。目前已有1225人学习下载可作为SSM框架整合练习与问卷类系统二次开发的实用参考。1. 问卷调查系统SSM Layui JSP 这套老架构为什么还值得动手做一遍如果你正在搜「Java项目 问卷调查系统 SSM Layui JSP Mysql」大概率不是单纯想找个源码交差而是想搞明白一套看起来“过时”的 JSP 技术栈怎么把问卷的创建、发布、填写、统计这条链路真正跑通。我见过太多人卡在环境上——IDEA 新建 JSP 项目目录不对、Tomcat 不认、MySQL 连不上报 error 2002、Layui 表格数据渲染不出来。这套系统的价值恰恰在于它把 JavaWeb 的核心链路压缩在一个你能完全掌控的项目里没有 Spring Boot 自动配置的黑匣子每一层都看得见摸得着。适合课程设计、毕设选题也适合想补 JavaWeb 基础再过渡到 SSM 或 Spring Boot 的开发者。下面我按实际搭建顺序把选型理由、建表、后端接口、前端渲染和排错一次讲透。2. 选型与架构拆解为什么是 SSM Layui JSP 而不是 Spring Boot2.1 这套组合各自解决什么问题先把四个技术点摆清楚不然后面写代码容易混。SSM是 Spring SpringMVC MyBatis 的缩写。Spring 管 Bean 和事务SpringMVC 管请求路由和参数绑定MyBatis 管 SQL 映射。三者合起来就是一个不依赖 Spring Boot 自动配置的手动装配 Web 层。问卷调查系统的业务逻辑不复杂——问卷增删改查、题目管理、答卷提交、结果统计——但正好覆盖了 Controller → Service → Mapper → MySQL 这条完整链路拿来练手非常合适。Layui是一个经典的前端 UI 库表格、表单、弹层、分页都有现成组件。问卷系统的后台管理页面问卷列表、题目编辑、答卷查看用 Layui 的 table 和 form 模块能省掉大量手写 HTML 和 CSS 的时间。注意 Layui 的数据表格默认要求后端返回特定格式的 JSON这个后面会讲。JSP负责页面渲染。有人会问 nginx 支持 jsp 吗——不支持JSP 必须由 Servlet 容器Tomcat、Jetty编译执行。所以这套项目的部署方式是打 war 包丢进 Tomcat不是丢 nginx。传统 JSP 项目打包 war 这个操作后面会给命令。MySQL存问卷、题目、选项、答卷、答案五类核心数据。版本用 5.7 或 8.0 都行8.0 注意驱动包和时区配置。2.2 项目分层与目录结构我一般按这个结构组织清晰且好排查survey-system/ ├── src/main/java/com/survey/ │ ├── controller/ # SpringMVC 控制器 │ ├── service/ # 业务接口 │ ├── service/impl/ # 业务实现 │ ├── mapper/ # MyBatis Mapper 接口 │ ├── entity/ # 实体类 │ └── util/ # 工具类统一返回、分页等 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 映射文件 │ ├── spring-core.xml # Spring 核心配置 │ ├── spring-mvc.xml # SpringMVC 配置 │ └── jdbc.properties # 数据库连接 ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── views/ # JSP 页面 │ ├── static/ │ │ ├── layui/ # Layui 静态资源 │ │ └── js/ # 自定义 JS │ └── index.jsp └── pom.xml这个结构的关键点JSP 放在 WEB-INF/views 下不能直接被浏览器访问必须走 Controller 转发这样权限控制才有意义。Layui 放 static 下SpringMVC 要配置静态资源放行否则 404。2.3 依赖配置pom.xml 里必须锁死的几个版本properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version mysql.driver.version8.0.33/mysql.driver.version servlet.api.version4.0.1/servlet.api.version jstl.version1.2/jstl.version /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.driver.version}/version /dependency !-- Servlet 与 JSTL -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version${servlet.api.version}/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version${jstl.version}/version /dependency !-- 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies参数说明servlet-api的 scope 必须是provided因为 Tomcat 自带打 war 时不能重复打包否则启动报类冲突。MySQL 8 驱动类名是com.mysql.cj.jdbc.DriverURL 要带serverTimezoneAsia/Shanghai不然时间字段会差 8 小时。Druid 连接池的初始连接数、最大连接数在 jdbc.properties 里配后面给。2.4 数据库连接与 Spring 整合配置jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/survey_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码 druid.initialSize5 druid.maxActive20 druid.minIdle5 druid.maxWait60000spring-core.xml 关键片段!-- 加载数据库配置 -- context:property-placeholder locationclasspath:jdbc.properties/ !-- Druid 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value${druid.initialSize}/ property namemaxActive value${druid.maxActive}/ property nameminIdle value${druid.minIdle}/ property namemaxWait value${druid.maxWait}/ /bean !-- MyBatis SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.survey.entity/ /bean !-- Mapper 扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.survey.mapper/ /bean !-- 事务管理 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这里有个容易翻车的点context:property-placeholder和MapperScannerConfigurer同时存在时如果 placeholder 加载顺序不对数据源里的${jdbc.url}会解析失败。解决办法是把 placeholder 放在最前面或者用ignore-unresolvabletrue。我一般直接放最前面省事。spring-mvc.xml 关键片段!-- 开启注解驱动 -- mvc:annotation-driven/ !-- 扫描 Controller -- context:component-scan base-packagecom.survey.controller/ !-- 静态资源放行Layui 必须走这里 -- mvc:resources mapping/static/** location/static// !-- JSP 视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean !-- 处理 JSON 响应乱码 -- mvc:message-converters bean classorg.springframework.http.converter.StringHttpMessageConverter property namesupportedMediaTypes list valuetext/plain;charsetUTF-8/value valueapplication/json;charsetUTF-8/value /list /property /bean /mvc:message-converters静态资源放行这条如果漏了Layui 的 css 和 js 全部 404页面会裸奔。这是新手最常见的翻车点之一。3. 数据库表设计与问卷核心链路实现3.1 五张核心表怎么建问卷系统的数据模型不复杂但设计时要想清楚“问卷—题目—选项—答卷—答案”的层级关系。我一般用五张表CREATE DATABASE survey_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE survey_db; -- 问卷表 CREATE TABLE t_survey ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 问卷标题, description VARCHAR(500) DEFAULT COMMENT 问卷说明, status TINYINT DEFAULT 0 COMMENT 0草稿 1发布 2关闭, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 题目表 CREATE TABLE t_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL COMMENT 所属问卷, content VARCHAR(500) NOT NULL COMMENT 题目内容, type TINYINT NOT NULL COMMENT 1单选 2多选 3填空, sort_order INT DEFAULT 0 COMMENT 排序, required TINYINT DEFAULT 1 COMMENT 是否必填 1是 0否, INDEX idx_survey (survey_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选项表 CREATE TABLE t_option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, content VARCHAR(200) NOT NULL, sort_order INT DEFAULT 0, INDEX idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 答卷表一次提交对应一条 CREATE TABLE t_answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, ip VARCHAR(50) DEFAULT , INDEX idx_survey (survey_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 答案明细表 CREATE TABLE t_answer_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL, question_id BIGINT NOT NULL, option_id BIGINT DEFAULT NULL COMMENT 选择题选中的选项填空题为NULL, answer_text VARCHAR(1000) DEFAULT NULL COMMENT 填空题答案, INDEX idx_sheet (sheet_id), INDEX idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明字符集统一用utf8mb4不然问卷标题里出现 emoji 或生僻字会插入失败。t_answer_detail里option_id和answer_text二选一选择题存 option_id填空题存 answer_text这样统计时不用做类型判断。多选题一次提交会有多条 detail 记录同一个 question_id 对应多个 option_id。3.2 问卷创建与题目保存的 Service 实现问卷创建不是单表插入而是问卷 题目 选项三张表一起写必须加事务。这是 Java 怎么保证数据一致性的典型场景。Service public class SurveyServiceImpl implements SurveyService { Autowired private SurveyMapper surveyMapper; Autowired private QuestionMapper questionMapper; Autowired private OptionMapper optionMapper; Override Transactional(rollbackFor Exception.class) public Long createSurvey(SurveyDTO dto) { // 1. 插入问卷主表 Survey survey new Survey(); survey.setTitle(dto.getTitle()); survey.setDescription(dto.getDescription()); survey.setStatus(0); surveyMapper.insert(survey); Long surveyId survey.getId(); // 2. 遍历题目逐条插入 ListQuestionDTO questions dto.getQuestions(); if (questions ! null) { for (int i 0; i questions.size(); i) { QuestionDTO qDto questions.get(i); Question question new Question(); question.setSurveyId(surveyId); question.setContent(qDto.getContent()); question.setType(qDto.getType()); question.setSortOrder(i); question.setRequired(qDto.getRequired()); questionMapper.insert(question); Long questionId question.getId(); // 3. 选择题插入选项 if (qDto.getType() ! 3 qDto.getOptions() ! null) { for (int j 0; j qDto.getOptions().size(); j) { Option option new Option(); option.setQuestionId(questionId); option.setContent(qDto.getOptions().get(j)); option.setSortOrder(j); optionMapper.insert(option); } } } } return surveyId; } }逻辑说明Transactional的rollbackFor Exception.class必须加因为默认只回滚 RuntimeException如果中间抛了受检异常前面的插入不会回滚数据就脏了。surveyMapper.insert后要能拿到自增主键MyBatis 的 XML 里要配useGeneratedKeystrue keyPropertyid否则getId()返回 null后面题目关联就断了。3.3 答卷提交与统计查询答卷提交同样是一个事务先插t_answer_sheet拿 sheetId再批量插t_answer_detail。Override Transactional(rollbackFor Exception.class) public void submitAnswer(AnswerSubmitDTO dto) { AnswerSheet sheet new AnswerSheet(); sheet.setSurveyId(dto.getSurveyId()); sheet.setIp(dto.getIp()); answerSheetMapper.insert(sheet); Long sheetId sheet.getId(); for (AnswerItemDTO item : dto.getItems()) { if (item.getOptionIds() ! null !item.getOptionIds().isEmpty()) { // 选择题每个选中选项一条记录 for (Long optionId : item.getOptionIds()) { AnswerDetail detail new AnswerDetail(); detail.setSheetId(sheetId); detail.setQuestionId(item.getQuestionId()); detail.setOptionId(optionId); answerDetailMapper.insert(detail); } } else { // 填空题 AnswerDetail detail new AnswerDetail(); detail.setSheetId(sheetId); detail.setQuestionId(item.getQuestionId()); detail.setAnswerText(item.getAnswerText()); answerDetailMapper.insert(detail); } } }统计查询用一条 GROUP BY 就能出结果SELECT o.id AS optionId, o.content AS optionContent, COUNT(d.id) AS count FROM t_option o LEFT JOIN t_answer_detail d ON o.id d.option_id WHERE o.question_id #{questionId} GROUP BY o.id, o.content ORDER BY o.sort_order;用 LEFT JOIN 保证没人选的选项也显示count 为 0。如果只统计已选中的用 INNER JOIN 就行但问卷统计通常要展示全部选项的占比所以 LEFT JOIN 更合适。4. Layui 前端渲染与 JSP 页面集成4.1 Layui 数据表格对接后端接口Layui 的 table 模块对后端返回格式有硬性要求必须是{ code: 0, msg: , count: 100, data: [...] }code 为 0 表示成功count 是总记录数用于分页data 是当前页数据。后端 Controller 要按这个格式封装。我一般写一个统一返回类public class LayuiResultT { private int code; private String msg; private long count; private ListT data; public static T LayuiResultT success(long count, ListT data) { LayuiResultT r new LayuiResult(); r.code 0; r.msg ; r.count count; r.data data; return r; } // getter/setter 省略 }Controller 示例RestController RequestMapping(/api/survey) public class SurveyApiController { Autowired private SurveyService surveyService; GetMapping(/list) public LayuiResultSurvey list( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int limit) { int offset (page - 1) * limit; ListSurvey list surveyService.listByPage(offset, limit); long total surveyService.countAll(); return LayuiResult.success(total, list); } }参数说明page和limit是 Layui table 默认传的分页参数不用自己定义。offset是 SQL 的 LIMIT 偏移量MyBatis 里用LIMIT #{offset}, #{limit}接收。注意RestController返回的是 JSON不要用Controller否则会去找 JSP 视图。4.2 JSP 页面里引入 Layui 和渲染表格JSP 页面头部% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetutf-8 title问卷管理/title link relstylesheet href${pageContext.request.contextPath}/static/layui/css/layui.css /head body table idsurveyTable lay-filtersurveyTable/table script src${pageContext.request.contextPath}/static/layui/layui.js/script script layui.use([table, layer], function () { var table layui.table; var layer layui.layer; table.render({ elem: #surveyTable, url: ${pageContext.request.contextPath}/api/survey/list, method: get, page: true, cols: [[ {field: id, title: ID, width: 80}, {field: title, title: 问卷标题, minWidth: 200}, {field: status, title: 状态, width: 100, templet: function (d) { return d.status 1 ? span stylecolor:green已发布/span : span stylecolor:orange草稿/span; }}, {field: createTime, title: 创建时间, width: 180}, {title: 操作, width: 200, templet: function (d) { return a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-xs layui-btn-danger lay-eventdel删除/a; }} ]] }); // 监听工具条事件 table.on(tool(surveyTable), function (obj) { if (obj.event del) { layer.confirm(确定删除, function (index) { $.get(${pageContext.request.contextPath}/api/survey/delete, {id: obj.data.id}, function (res) { if (res.code 0) { table.reload(surveyTable); } layer.close(index); }); }); } }); }); /script /body /html逻辑说明${pageContext.request.contextPath}是 JSP 获取项目上下文路径的标准写法避免部署到 Tomcat 后路径不对导致 404。templet是 Layui 的列模板函数用来做状态转换和操作按钮渲染。table.on(tool(...))监听行内按钮点击obj.data是当前行数据。4.3 问卷填写页的动态渲染填写页需要根据题目类型动态渲染单选、多选、填空。JSP 里用 JSTL 遍历题目列表form classlayui-form idanswerForm c:forEach items${questions} varq varStatusst div classlayui-form-item label classlayui-form-label${st.index 1}. ${q.content}/label div classlayui-input-block c:choose c:when test${q.type 1} c:forEach items${q.options} varopt input typeradio nameq_${q.id} value${opt.id} title${opt.content} /c:forEach /c:when c:when test${q.type 2} c:forEach items${q.options} varopt input typecheckbox nameq_${q.id} value${opt.id} title${opt.content} /c:forEach /c:when c:otherwise textarea nameq_${q.id} classlayui-textarea/textarea /c:otherwise /c:choose /div /div /c:forEach button classlayui-btn lay-submit lay-filtersubmitAnswer提交/button /form注意 Layui 的 radio 和 checkbox 需要form.render()才能显示样式在layui.use([form])里调用。提交时用 JS 收集表单数据组装成 JSON 发给后端不能直接 form submit因为后端接口是RequestBody接收 JSON 的。5. 避坑与排查从环境到运行时的 5 个血泪教训5.1 Tomcat 启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver现象项目编译通过Tomcat 启动时控制台报驱动类找不到。原因MySQL 驱动的 jar 没有放到WEB-INF/lib下。IDEA 里 Maven 依赖默认不会自动复制到 war 的 lib 目录除非在 Project Structure 的 Artifacts 里手动添加或者用mvn package打 war 时由 Maven 处理。解决在 IDEA 的 Project Structure → Artifacts → 你的 war exploded → WEB-INF/lib 里把 mysql-connector-java 加进去。或者直接用mvn clean package生成 war 包Maven 会自动把 compile scope 的依赖打进 lib。5.2 Layui 表格请求成功但数据不显示现象浏览器 Network 里看到接口返回了 JSON格式也对但表格是空的。原因Layui table 默认要求返回的code字段为 0count和data字段名不能改。如果后端返回的是{total: 100, rows: [...]}Layui 不认。解决要么改后端返回格式要么在 table.render 里配response: {statusCode: 200}和parseData做字段映射。我一般直接改后端用统一返回类省得前端到处配。5.3 MySQL 连接报 error 2002 (HY000): Cant connect through socket现象命令行或 Navicat 连 MySQL 报 socket 错误。原因MySQL 服务没启动或者 socket 文件路径不对。Linux 下常见于/tmp/mysql.sock不存在Windows 下一般是服务没起。解决Linux 用systemctl start mysqld或service mysql startWindows 在服务管理器里启动 MySQL 服务。如果 socket 路径不对在连接配置里显式指定socket/var/lib/mysql/mysql.sock。Navicat 连接时选 TCP/IP 而不是 Socket。5.4 JSP 页面中文乱码现象页面上的中文显示成问号或乱码。原因JSP 文件本身的编码、pageEncoding、response 的 contentType 三者不一致。解决JSP 第一行写% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %文件保存为 UTF-8。web.xml 里加字符编码过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping5.5 事务不生效问卷插入了但题目没插入现象创建问卷时问卷主表有数据但题目表是空的或者部分题目插入失败后前面的没回滚。原因Transactional没生效。常见原因有三个一是 Service 类没被 Spring 扫描到不在 component-scan 范围内二是方法不是 public三是异常被 catch 了没抛出去Spring 感知不到。解决确认 spring-core.xml 里context:component-scan base-packagecom.survey.service/包含 Service 包Transactional加在 public 方法上catch 块里要么不 catch要么 catch 后throw new RuntimeException(e)。另外rollbackFor Exception.class要加上覆盖受检异常。6. 进阶技巧用一条 SQL 出统计报表和 war 包部署6.1 问卷结果统计的聚合查询问卷统计最常用的两个视图每个选项的选择人数和占比以及填空题的答案列表。选项统计前面给过这里补一个带占比的SELECT o.content AS optionContent, COUNT(d.id) AS voteCount, ROUND(COUNT(d.id) * 100.0 / ( SELECT COUNT(DISTINCT d2.sheet_id) FROM t_answer_detail d2 WHERE d2.question_id #{questionId} ), 2) AS percentage FROM t_option o LEFT JOIN t_answer_detail d ON o.id d.option_id WHERE o.question_id #{questionId} GROUP BY o.id, o.content ORDER BY o.sort_order;这个查询的分母是“答了这道题的人数”不是“总答卷数”因为填空题和选择题的答题人数可能不同。COUNT(DISTINCT d2.sheet_id)保证同一份答卷只算一次。如果问卷允许跳过非必填题这个分母才是准确的。6.2 打 war 包部署到 Tomcat# 在项目根目录执行 mvn clean package -DskipTests # 生成的 war 包在 target/ 下复制到 Tomcat cp target/survey-system.war $TOMCAT_HOME/webapps/ # 启动 Tomcat $TOMCAT_HOME/bin/startup.sh # Linux # 或 %TOMCAT_HOME%\bin\startup.bat # Windows访问路径是http://localhost:8080/survey-system/注意 war 包名就是上下文路径。如果想改成根路径把 war 包重命名为ROOT.war。Tomcat 会自动解压 war 包修改 JSP 后如果不想重启可以配置reloadabletrue但生产环境不建议开性能损耗明显。6.3 一个我踩过的坑有次部署到服务器后问卷列表能打开但填写页 500。查了半天发现是 JSP 里用了${q.options}但后端返回的 Question 实体里 options 字段是 null——因为查询题目时没有关联查选项。后来在 MyBatis 的 resultMap 里加了collection嵌套查询或者 Service 层手动组装。这个问题的教训是JSP 页面报错信息很模糊一定要看 Tomcat 的 catalina.out 日志里面会有完整的异常栈。我现在做这类项目习惯先把数据库和 MyBatis 映射跑通用单元测试验证每个 Mapper 方法再写 Service 和 Controller最后才动 JSP。这样出问题时能快速定位是哪一层。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网