前阵子刚好把一套基于SSM的垃圾分类查询系统从零到部署完整走了一遍,这套东西看着简单,真上手才知道里面藏着不少细节——光垃圾数据怎么整理、分类关联怎么设计、分页和搜索怎么配合,就够琢磨一阵。这篇把整个项目的设计思路、表结构、核心代码、部署踩坑全部整理出来,给正在做javaweb毕设或者想练手SSM整合的朋友做个参考,尤其是第一次用JSP+Bootstrap做管理后台的新手,可以少走很多弯路。
1. 项目定位与技术选型:为什么是SSM+JSP+Bootstrap
1.1 这个系统到底要解决什么问题
垃圾分类查询系统的核心需求其实很朴素:居民拿到一件垃圾,不知道属于什么类别,打开网页输入名称,立刻能查到对应分类和处置建议。但真正动手做的时候,你会发现给它扩展成管理系统后,需求层次一下子变多了。前端需要提供快速查询入口和分类浏览页面,后端需要一个完整的管理后台,让运营人员维护垃圾条目、分类说明、公告信息,还要统计用户的查询记录。
所以我把系统拆成两个主要角色。普通用户可以注册登录、按名称查询垃圾类别、按分类查看垃圾列表、浏览公告;管理员登录后台之后可以管理垃圾条目、管理分类、管理用户、发布公告。这个权限模型不算复杂,但对学习SSM框架来说刚好合适,能覆盖增删改查、关联查询、分页、登录拦截这些最常见的业务场景。
1.2 技术选型背后的三个考虑
为什么用SSM而不是SpringBoot?坦率说,放在现在生产环境我更倾向SpringBoot,但这个项目定位是教学和毕设,SSM的价值在于它把Spring、SpringMVC、MyBatis三个框架的整合过程全部暴露出来,配置文件、依赖关系、拦截器、事务控制都得自己一个坑一个坑地趟。你把这个流程走通,再去看SpringBoot的自动配置,理解深度完全不一样。
JSP也是同样的逻辑。很多教程都在讲前后端分离,但高校课程和企业里仍有大量遗留系统用JSP,而且JSP+JSTL可以直接在页面上做数据渲染,配合EL表达式,对新手理解MVC的分工非常有帮助。Bootstrap则是为了快速解决样式问题,它的栅格系统能在没有专门前端工程师的情况下,把后台和前台页面做得像模像样。
MyBatis层面我选择了XML映射方式而不是注解方式。原因很简单:垃圾分类的查询条件是多变的,管理员搜索、前台模糊查询、分类筛选,SQL需要动态拼接,XML里的<where>和<if>标签比注解的@SelectProvider直观太多了。
1.3 系统整体的请求流转结构
模块划分上,我按照经典的三层架构来做:Controller层负责接收请求和参数校验,Service层处理业务逻辑和事务,Mapper层管数据库操作。实体类对应数据库表,VO类负责页面展示数据的组装。
一个典型的查询请求流转是这样的:用户在页面输入“废旧电池”,提交到GarbageController的search方法,Controller把关键词传给GarbageService,Service调用GarbageMapper执行模糊查询,结果封装成JSON返回给前端,前端用jQuery把结果渲染到Bootstrap的卡片组件里。整个链路跑通之后,你再去理解SpringMVC的DispatcherServlet机制,就会有画面感了。
2. 数据库设计:垃圾分类数据模型的关键点
2.1 表结构规划与关联关系
数据库设计是这类项目最容易出彩也最容易翻车的环节。一开始我也想过只建一张垃圾表,把所有信息塞进去,但后来整理数据时发现,垃圾条目和分类之间是明显的多对一关系,而且分类需要独立维护属性,比如颜色标识、投放要求、处理方式。所以我最终用四张核心表来实现。
user表存用户和管理员信息,字段包括主键、用户名、密码、联系电话、创建时间、是否管理员。category表存垃圾分类类别,字段包括主键、分类名称、分类描述、图标路径、投放要求。garbage表是核心表,字段包括主键、垃圾名称、所属分类ID、详细描述、投放提示、创建时间。query_record表记录每次查询行为,字段包括主键、用户ID、查询关键词、查询结果、查询时间。
这个设计的妙处在于,垃圾条目只存category_id,不存冗余的分类名称,需要展示分类信息时通过JOIN或者二次查询获取。表关系干净了,后面做分类统计、条目筛选都方便。
2.2 字段设计与数据类型选择的细节
先说garbage表。垃圾名称字段我用了varchar(100),有人觉得50就够了,其实不够,因为很多复合词很长,比如“沾了油的快递纸箱”,100的长度更保险。关键词搜索走的字段就是name,所以我把这个字段加了普通索引。分类ID用int并设置外键逻辑关联,注意,我在物理层面没建外键约束,生产环境不推荐物理外键,因为会影响插入性能且难以维护,逻辑关联足够。
category表的排序字段容易被忽略。分类列表在页面上的展示顺序需要人工控制,所以我加了sort_order整型字段,查询时ORDER BY sort_order ASC,这样可以在不改变主键的情况下灵活调整顺序。投放要求字段用text类型,因为内容是长文本,而且可能包含换行。
query_record表主要用于后台统计热门搜索,所以关键词和结果字段都要保留,查询时间用datetime,并设置默认值为CURRENT_TIMESTAMP。注意,MySQL 8.0对datetime默认值的写法有变化,DEFAULT CURRENT_TIMESTAMP是可以直接用的,但如果你用的版本较老,可能需要手动在插入语句里赋值。
2.3 垃圾数据的整理与初始化脚本
数据是这类系统的灵魂。网上虽然能搜到一些现成的SQL脚本,但质量参差不齐,有的分类逻辑混乱,比如把“创可贴”归到可回收垃圾,这明显不对。我最终以国家垃圾分类标准为基础,结合社区公开数据,总共整理了一千多条常见垃圾条目,分成可回收物、有害垃圾、厨余垃圾、其他垃圾四类。
每条垃圾数据除了名称和分类,我还尽量补充了投放提示,比如“饮料瓶投放前请倒空液体并压扁”“过期药品连同包装一并投放”。这些描述虽然会增加录入工作量,但对用户来说价值极高,直接影响系统的实用性。数据整理完成后,用LOAD DATA INFILE或者直接INSERT脚本导入,注意字符集统一为utf8mb4,否则中文会乱码。
3. 后端核心功能实现:从环境搭建到底层SQL
3.1 SSM整合的工程结构与配置文件要点
Maven工程是这个项目的基础。我创建一个ssm-garbage的war包项目,标准目录结构是src/main/java、src/main/resources、src/main/webapp。Java包名用com.garbage,下面分controller、service、mapper、entity、vo、common六个子包。
pom.xml里的依赖要特别注意版本冲突。Spring我用的5.2.x,MyBatis用3.5.x,mybatis-spring用2.0.x,mysql-connector-java用8.0.x。这里有个重点:MySQL 8.0的驱动类名变成了com.mysql.cj.jdbc.Driver,不是老款的com.mysql.jdbc.Driver,同时连接URL必须加上serverTimezone=Asia/Shanghai参数,否则会报时区错误。
核心配置文件有三个。jdbc.properties只管数据库连接参数;spring-context.xml负责组件扫描、数据源、SqlSessionFactory、事务管理器;spring-mvc.xml负责Controller扫描、注解驱动、视图解析器、静态资源放行。我写一下spring-context里最重要的数据源配置:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.garbage.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.garbage.mapper"/> </bean>数据源我用的是Druid而不是默认的简单连接池,因为它自带监控页面,调试的时候能直接看到SQL执行情况和连接池状态。如果你第一次接触数据源的概念,可以把它理解成一个“数据库连接的中转仓库”,每次请求不需要新建连接,直接从仓库里取,用完归还,性能能提升一个档次。
3.2 垃圾分类查询的两种核心SQL写法
查询功能是系统的门面,我做了两种方式。一种是纯粹的名称模糊查询,用户输入一个字或一个词,系统在garbage表里匹配;另一种是按分类筛选,用户点击“有害垃圾”分类,系统列出该分类下所有条目。
第一种方式的关键在于LIKE语句的位置,我一开始写在Java代码里拼接,后来发现不好维护,改成在Mapper.xml里动态拼接:
<select id="searchByName" resultType="com.garbage.entity.Garbage" parameterType="string"> SELECT * FROM garbage <where> <if test="keyword != null and keyword != ''"> name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY id DESC </select>这里特别提醒一下,LIKE CONCAT('%', #{keyword}, '%')比直接写LIKE '%${keyword}%'安全得多,后者存在SQL注入风险。用#{}占位符会让MyBatis自动预编译,传进来的参数被当成纯数据而不是SQL片段。
第二种分类筛选更简单,WHERE category_id = #{categoryId},但这个查询往往带着分页需求。我手写了MySQL分页,LIMIT #{offset}, #{pageSize},offset的计算放在Service层完成,(pageNum - 1) * pageSize,这一步逻辑很基础但经常有人搞错,尤其页码从1开始还是从0开始,一定要统一约定。
3.3 后台管理的增删改查与登录拦截
后台管理模块的代码量大头集中在Controller的CRUD方法上。以垃圾条目管理为例,五个基础方法:list方法支持分页和条件查询,toAdd方法跳转新增页面,add方法处理新增请求,toEdit方法回显数据,edit方法更新数据,delete方法做删除。
点一下常见问题:回显的时候经常遇到数据丢失,比如编辑页打开后下拉框没有选中原来的分类。这个下拉框其实是从category表查出来的所有分类,回显时要用selected属性判断当前项是否等于garbage.categoryId,这个在JSP里可以写:
<select name="categoryId" class="form-control"> <c:forEach items="${categoryList}" var="cat"> <option value="${cat.id}" ${cat.id == garbage.categoryId ? 'selected' : ''}>${cat.name}</option> </c:forEach> </select>登录拦截我是用SpringMVC的Interceptor实现的,写一个LoginInterceptor,在preHandle方法里判断Session里有没有loginUser,没有就重定向到登录页。拦截器注册是在spring-mvc.xml里完成的,要配置拦截路径,我的设置是拦截所有/**请求,但是放行/user/login、/user/register、静态资源路径/static/**。这里最容易踩的坑是顺序问题,放行规则一定要写在拦截规则前面,不然静态资源全被拦截。
4. 前端页面与Bootstrap布局:快速构建可用界面
4.1 页面架构与JSP文件规划
页面规划上,我分成前台和后台两块。前台包含首页、查询结果页、分类浏览页、注册登录页;后台包含框架布局页、垃圾列表页、垃圾编辑页、分类管理页、用户管理页、公告管理页。
JSP文件放在webapp/WEB-INF/jsp目录下,这是Web项目的一个安全习惯——放在WEB-INF下的JSP无法直接通过URL访问,必须经Controller转发,能防止用户绕过权限直接打开页面。前台和后台各做一个公共header.jspf和footer.jspf片段,用<%@ include %>指令或者JSTL的<c:import>引入,避免每个页面重复写导航栏和页脚。
页面框架我用的是Bootstrap的经典布局,左侧固定导航栏加右侧内容区。Bootstrap 4的栅格系统是12列,这是我们可以反复利用的规则。栅格的底层逻辑是flex布局,真正理解之后你会发现,小白也能做整齐的排版。我举一个例子:前台查询页面,左侧三分之二是查询结果列表,右侧三分之一是热门分类入口,这个就是col-lg-8和col-lg-4的组合。
4.2 Bootstrap组件在项目里的实际用法
我用的最顺手的是Bootstrap的卡片组件和模态框。查询结果列表用卡片比表格好看太多,每张卡片显示垃圾名称、分类标签、投放提示,分类标签用不同颜色的badge区分——橙色是可回收物,红色是有害垃圾,绿色是厨余垃圾,灰色是其他垃圾。badge组件的好处是颜色醒目且不占空间。
模态框在编辑页面非常实用。传统的编辑方式是跳转一个新页面,但用Bootstrap模态框可以在当前页面弹出一个编辑面板,用户体验更好。我用它来做分类信息的快速编辑,触发方式是垃圾条目列表每一行右侧的“编辑”按钮,点击后通过Ajax请求获取该分类的完整数据,然后填充到模态框的表单里,提交时再用Ajax发送更新请求。这套交互模式在现在的Web开发里非常常见,学好它等于同时掌握了前端和后端的配合方式。
表单验证我也是用的Bootstrap相关的验证样式。必填项用HTML5的required属性,配合Bootstrap的.is-invalid和.is-valid类,可以在不引入额外插件的情况下做到基础的输入反馈。密码强度的校验就自己写正则了,至少8位且包含字母和数字。
4.3 前台查询交互与Ajax调用的处理
前台查询页的核心交互是:用户输入关键词,点击查询按钮,页面不刷新,直接通过Ajax从后端取回数据并渲染。这类似我们平时说的“局部刷新”,能明显感觉到比传统表单提交流畅。关键代码是用jQuery的$.ajax:
function searchGarbage() { var keyword = $("#keyword").val().trim(); if (keyword === "") { alert("请输入垃圾名称"); return; } $.ajax({ url: "/garbage/search", type: "GET", data: { "keyword": keyword }, dataType: "json", success: function(data) { if (data.code === 200) { renderResult(data.data); } else { $("#resultArea").html("<p class='text-muted'>没有找到相关结果</p>"); } }, error: function() { alert("请求失败,请稍后重试"); } }); }后端的Controller方法加@ResponseBody注解,返回统一的结果对象。我在common包里定义了一个Result类,包含code、message、data三个字段,所有接口统一返回这个结构,前端判断code是否为200来决定渲染逻辑。这个习惯在前后端协作项目里会给你省下很多沟通成本。
前端渲染结果的逻辑,我用的是JSON.parse之后循环拼接字符串,虽然代码不算优雅,但简单直接。用ES6的模板字符串可以简化拼接:
function renderResult(list) { var html = ""; list.forEach(function(item) { html += ` <div class="card mb-2"> <div class="card-body"> <h5 class="card-title">${item.name}</h5> <span class="badge badge-primary">${item.categoryName}</span> <p class="card-text mt-2">${item.tips}</p> </div> </div>`; }); $("#resultArea").html(html); }这里要注意XSS问题,如果垃圾名称或描述中包含HTML标签,直接拼接可能被浏览器解析执行。稳妥的做法是转义处理,我写了一个小工具方法,把<、>、&、"等字符替换为实体字符。
5. 常见问题与排查技巧实录
5.1 数据库连接失败的几种典型情况
这个项目里九成的新手问题都出在数据库连接上。第一种是驱动类找不到,报ClassNotFoundException: com.mysql.jdbc.Driver,这是因为依赖配错版本了,MySQL 8.x要用com.mysql.cj.jdbc.Driver,或者干脆检查一下pom里mysql-connector-java的版本号。第二种是时区错误,报The server time zone value 'Öйú±ê׼ʱ¼ä',这个乱码其实是中文时区名,解决办法在URL加serverTimezone=Asia/Shanghai。
第三种是连接被拒,报Communications link failure,这种通常是数据库服务没启动,或者端口不是默认的3306。Linux下排查可以用netstat -tlnp | grep 3306查看端口是否在监听,Windows下可以检查服务管理器里MySQL服务的状态。第四种非常隐蔽:密码通过配置文件传入时包含特殊字符,比如@,会把jdbc:mysql://...的URL解析搞乱,此时要用URL编码或者调整密码。
我建议调试数据库问题时,先直接在命令行里用mysql -u root -p测试连接,确认命令行能连上,再排查Java代码。这一步能帮你缩小问题范围,避免在代码里瞎猜。
5.2 Tomcat部署JSP项目的常见坑
我用的是Tomcat 9配合Maven的tomcat7-maven-plugin插件启动。这个插件虽然名字叫tomcat7,但实际可以兼容Tomcat 8.5和9的一些功能,在pom.xml配置好<path>/</path>就能直接用mvn tomcat7:run启动。
启动过程中最常见的问题是JSP编译失败,表现是页面报500错误,Tomcat日志里有Unable to compile class for JSP。原因多半是taglib指令没写对,比如JSTL核心标签库的URI在JSP页面里写成了http://java.sun.com/jsp/jstl/core,这个老地址在Jakarta改名之后已经失效了,要用http://java.sun.com/jsp/jstl/core在旧项目里可以,但新项目建议确认一下依赖版本。
另一个常见问题是静态资源404。JSP页面引用的CSS、JS文件返回404,原因大概率是SpringMVC的前端控制器DispatcherServlet拦截了所有请求,包括静态资源。解决办法是在spring-mvc.xml加静态资源放行配置:
<mvc:resources mapping="/static/**" location="/static/"/>这个配置会告诉DispatcherServlet,以/static/开头的请求直接交给默认Servlet处理,不再进入Controller。
5.3 查询结果为空或者SQL报错的排查
如果前台查询返回结果总为空,先别急着改代码,直接在数据库命令行执行一遍同样的SQL,看有没有数据。很多时候问题出在数据没导入,或者导入时字符集混乱,数据是乱码导致匹配不上。我当初导入数据时忘记指定utf8mb4,页面查“苹果”能查出结果但显示乱码,后来用ALTER TABLE garbage CONVERT TO CHARACTER SET utf8mb4修正,才解决。
如果SQL报错Unknown column,说明实体类和表字段对不上,MyBatis默认使用驼峰映射,但表字段是下划线风格,需要在mybatis-config.xml设置:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>这一步没配置的话,category_id永远映射不到categoryId属性上,查询出来的对象全是null,页面白屏或者空列表,排查半天发现是映射问题。
还有个细节容易被忽略:query_record表在插入记录时,如果用户没有登录,user_id为空。我在表设计时把user_id设为可空,并在代码里判断,未登录用户查询时不走写入记录的逻辑,避免外键关联查询时出现笛卡尔积。这一点在统计热门查询时尤其重要,不能因为一个空值把SQL搞崩。
6. 项目扩展与优化建议
6.1 搜索体验优化:从模糊查询到全文检索
目前的LIKE '%关键词%'在数据量小的时候没问题,但垃圾条目超过几千条后,性能会明显下降。优化方向有两个:第一,利用MySQL的全文索引,在name字段上建FULLTEXT索引,用MATCH...AGAINST语法做全文检索,支持更复杂的匹配规则;第二,引入中文分词组件,比如在Java体系里集成HanLP做分词,再结合倒排索引实现更精确的搜索。
业余项目不建议一上来就引入Elasticsearch,那是分布式搜索引擎,牛刀杀鸡还会把部署复杂度拉满。先用好MySQL自带能力把性能优化上去,等数据量真正到十万级以上再架构升级。
6.2 前后端分离的演进方向
SSM+JSP这套技术栈学完之后,我建议你把同一个项目用SpringBoot+Vue重构一遍。不是说要否定SSM,而是通过对比理解架构演进的逻辑。JSP的页面渲染适合服务端渲染场景,但对复杂交互不够灵活,前后端共享一套数据模型也很别扭。改造成Vue之后,前端负责路由和交互,后端只出接口,两个团队可以并行开发。
改造时有个讨巧的过渡方案:不重写所有页面,先把查询结果页改成静态页面加Ajax请求接口,其他页面后续逐步替换。这样一次只改一个功能模块,不会遇到大爆炸式重构的风险。
6.3 从项目到部署的全链路思考
最后说点实在的。很多同学做完项目只会用mvn tomcat7:run跑起来看,这在本地没问题,但真正部署到服务器就会踩各种环境坑。生产部署我当时选择了云服务器加宝塔面板的方式,把war包直接扔进Tomcat的webapps目录就能跑。数据库首次启动需要执行初始化脚本,我建议把建表SQL和测试数据分开成两个文件,schema.sql管结构,data.sql管数据,便于单独维护。
配置方面,生产环境的数据库密码绝对不能写在jdbc.properties里明文保存,而是要通过环境变量注入,或者在部署时手动修改。我习惯在部署脚本里用sed命令自动替换占位符,这样既不会把敏感信息提交到代码仓库,又能保持部署流程的可重复性。
部署完之后一定要测试几个核心链路:注册、登录、查询、后台增删改查、会话过期后的重定向逻辑。会话超时这种场景在本地开发时几乎遇不到,但用户真实使用的过程中一定会发生,Session失效后请求接口会返回什么、前端如何处理,这些细节才是项目能真正上线和不能上线的分水岭。
我个人做这个项目的最大体会是:一个看似简单的管理系统,想做到真正顺手,拼的是数据质量和细节处理能力。垃圾条目这一千多条数据,我前前后后修订了三版,才把投放提示写得既准确又易懂。技术栈本身都是现成的,难的是把每个环节都按合理的标准去实现。如果你只是照葫芦画瓢跑起来,那很快就忘了;要是能在跑通的基础上,把一个查询优化方案或者一个交互细节吃透,这套SSM项目带给你的成长会远超课程本身。