简介:一套基于SSM框架的学生信息管理系统完整源码与数据库初始化脚本,适用对象包括Java Web初学者、毕业设计学生,以及想快速搭建后台管理模块的开发者。资源包共2个文件,整体大小约16.32MB,包含一个SQL脚本(用于创建students学生信息表及相关数据结构)和一个ZIP源码包(内附Spring配置、Controller控制器、Service业务层、DAO数据访问层与前端页面模板),可直接导入IDE运行。源码包内目录结构清晰,分层模块便于逐层学习与二次开发。通过实际操作和阅读代码,能掌握SSM三框架的整合步骤、请求参数绑定、依赖注入、MyBatis动态SQL及事务管理等关键技能,同时也能借鉴系统的分层设计,为扩展课程管理、成绩统计等功能提供清晰的参考。目前已有2392人学习下载,适合作为课程设计、毕业设计或入门级企业项目的蓝本。
1. Spring crm 与三层架构:为什么这套学生系统是最好入手的骨架
Spring crm 这类业务系统,拆到最底层,骨架就是同一套:Spring 管理对象,SpringMVC 接收请求,数据库负责存取。这套学生信息管理系统正好把这个骨架完整地串了一遍,从登录、列表,到新增、编辑、删除,把 Spring、SpringMVC 和数据库操作的 CRUD 闭环串在一起,顺带覆盖了事务、拦截器、参数绑定这些 CRM 业务里高频出现的能力。适合两类人:一类是刚学完 JavaWeb、想看看真实工程结构怎么落地的新手,另一类是需要在老项目里快速定位问题的熟手。核心不是它“多复杂”,而是它把三层架构该有的分工摆得很清楚。SpringMVC 项目看起来不如 Spring Boot 新潮,但把它的容器关系和请求链路看透,再回头看任何框架都轻松得多。
2. 先看懂工程骨架:Spring 容器与 SpringMVC 请求链路
2.1 三层分包:controller、service、dao 各管什么
先看目录结构。这套资源的主代码在 src/main/java 下,按 controller、service、dao、entity 四个包分开,resources 下放配置文件,webapp/WEB-INF/views 下放 JSP。结构如下:
src/main/java/com/demo/student ├── controller │ └── StudentController.java ├── service │ ├── StudentService.java │ └── impl │ └── StudentServiceImpl.java ├── dao │ └── StudentDao.java ├── entity │ └── Student.java ├── interceptor │ └── LoginInterceptor.java src/main/resources ├── applicationContext.xml ├── springmvc.xml └── jdbc.properties webapp ├── static │ ├── css │ └── js └── WEB-INF ├── web.xml └── views ├── login.jsp └── student ├── list.jsp └── form.jsp分包的核心逻辑:controller 只做参数接收和视图跳转,不写业务逻辑;service 层负责事务边界和业务规则;dao 层只写 SQL 和数据映射。把 controller 里的业务代码往下赶,是这套工程能长期维护的关键。很多新手把查询和保存逻辑全写在 controller 里,项目小的时候没问题,一旦加权限判断或事务要求,controller 就变成一坨改不动的代码。这套资源在这一点上做得规范,每个类的职责边界清楚,照着这个分包习惯写任何新模块都不会跑偏。
2.2 web.xml 启动顺序:Listener 先建容器,Servlet 再接收请求
SpringMVC 工程的入口不是 controller,而是 web.xml。这里有两个组件需要重点关注:ContextLoaderListener 和 DispatcherServlet。典型配置如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>启动顺序是:容器先创建根容器,加载 applicationContext.xml,把 service、dao、数据源这些对象放进去;随后 DispatcherServlet 创建子容器,加载 springmvc.xml,只负责 controller 和视图解析。子容器能访问父容器的 Bean,但父容器看不到子容器里的 controller。这就是为什么项目里两份 XML 分工明确,如果混在一个容器里,事务代理经常出问题。
这里有个参数容易忽略: / 表示拦截所有请求,但不拦截 .jsp,所以 JSP 由容器自己处理。还有一个默认路径规则:如果删掉 contextConfigLocation,Listener 默认去 WEB-INF/applicationContext.xml 找配置;DispatcherServlet 不写 init-param,默认找 /WEB-INF/springmvc-servlet.xml。这套资源是显式指定 classpath 路径,好处是配置文件放在 resources 下也能被加载。新手改配置时最容易把 resources 下的文件和 WEB-INF 下的文件搞混,报找不到文件时,先看 classpath 前缀和实际目录位置。
2.3 容器配置文件:applicationContext.xml 与 springmvc.xml 的职责边界
先看 applicationContext.xml 的内容要点:
<context:component-scan base-package="com.demo.student"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <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="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean>这个配置做了三件事:扫描 service、dao 所在包,排除 @Controller;读取 jdbc.properties 四个参数;把数据源注入 JdbcTemplate。这里的 Bean 定义用了 Spring 内置的 DriverManagerDataSource,是为了让初学者不用一上来就面对连接池的依赖配置;实际部署时换成 Druid 或 HikariCP 只需要改这个 Bean 的实现类,依赖注入的代码不用动。
再看 springmvc.xml 的对应部分:
<context:component-scan base-package="com.demo.student" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <mvc:annotation-driven/> <mvc:resources mapping="/static/**" location="/static/"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>注意 use-default-filters="false",它和 include-filter 配合,保证子容器只扫 @Controller,不去碰 service 和 dao。这个写法很多教程不会强调,如果用默认扫描,service 就被装进子容器一份,父容器的 @Transactional 代理管不住它,事务失效的坑就是这么埋下的。
InternalResourceViewResolver 的两个参数好理解:controller 返回 student/list,视图解析器拼出 /WEB-INF/views/student/list.jsp。把 JSP 放在 WEB-INF 下还有一个好处,用户不能通过 URL 直接访问 JSP 源码,所有页面都必须走 controller 转发。
提示:判断 Bean 归属有个笨办法,启动时把日志级别调到 DEBUG,看哪些类被“Root WebApplicationContext”初始化,哪些被“DispatcherServlet”初始化,一清二楚。
3. 把 CRUD 跑通:学生列表、新增、编辑、删除的完整请求链路
3.1 数据层:JdbcTemplate 的 SQL 参数绑定
先看 dao 层。StudentDao 用 @Repository 标记,由 Spring 容器管理,JdbcTemplate 在容器里已经配置好了。列表查询和数据插入的写法:
@Repository public class StudentDao { @Autowired private JdbcTemplate jdbcTemplate; public List<Student> findAll() { String sql = "select id, name, gender, birthday, phone, email " + "from t_student order by id desc"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Student.class)); } public void insert(Student student) { String sql = "insert into t_student(name, gender, birthday, phone, email) " + "values(?, ?, ?, ?, ?)"; jdbcTemplate.update(sql, student.getName(), student.getGender(), student.getBirthday(), student.getPhone(), student.getEmail()); } }BeanPropertyRowMapper 会把查询结果按列名自动映射到实体属性,但有一个前提:查询列名要么和属性名完全一致,比如 id、name、gender;要么用 SQL 别名把下划线转驼峰。如果表字段是 created_time 而实体属性是 createdAt,查询语句里必须写 created_time as createdAt。这个细节在老项目里很常见,很多表十年前就是下划线风格,接手时才发现 RowMapper 映射后全是 null。
参数绑定用问号占位符按顺序传参,避免字符串拼接带来的 SQL 注入问题。如果写成 "select * from t_student where name='" + name + "'",别人在表单里输入一个单引号就能截断条件,SQL 执行结果完全不可控。JdbcTemplate 的参数绑定解决了注入问题,但要求传入参数的顺序和 SQL 里的问号一一对应。后续改表结构时这里最容易出错,新增字段后,SQL 语句和参数列表要同步改,少传一个参数就会报参数个数不匹配。
3.2 事务与参数绑定:Service 层和 Controller 层的协作
Service 层负责事务边界,Controller 层负责参数接收。ServiceImpl 的核心实现:
@Service("studentService") @Transactional public class StudentServiceImpl implements StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = true) public List<Student> findAll() { return studentDao.findAll(); } @Transactional public void save(Student student) { studentDao.insert(student); } }@Transactional 标注在类上,表示所有 public 方法默认走事务;方法级注解再覆盖 readOnly 属性。readOnly = true 是给查询方法用的,暗示底层这条连接上不会执行写操作。这个注解全路径是 org.springframework.transaction.annotation.Transactional,实际项目里真有人导成 javax.transaction.Transactional,事务完全不生效,这个问题放到第 5 章展开。
Controller 层通过 @Autowired 注入 service 接口,而不是实现类,这样后续替换实现类时不需要动 controller。新增和编辑共用 form.jsp,表单提交到 save 方法:
@Controller @RequestMapping("/student") public class StudentController { @Autowired private StudentService studentService; @RequestMapping("/list") public String list(Model model) { model.addAttribute("students", studentService.findAll()); return "student/list"; } @RequestMapping(value = "/save", method = RequestMethod.POST) public String save(Student student, Model model) { studentService.save(student); return "redirect:/student/list"; } @RequestMapping("/edit/{id}") public String edit(@PathVariable("id") Integer id, Model model) { model.addAttribute("student", studentService.findById(id)); return "student/form"; } @RequestMapping("/delete/{id}") public String delete(@PathVariable("id") Integer id) { studentService.delete(id); return "redirect:/student/list"; } }这里有两个关键设计。第一,save 方法直接接收 Student 对象,SpringMVC 会把表单里的 name、gender、birthday 等字段自动绑定到实体属性上,这叫参数绑定。但绑定有前提:表单 input 的 name 必须和实体属性名一致,否则字段值就是 null。第二,新增成功后返回 redirect:/student/list,而不是视图名。这就是 PRG 模式(Post/Redirect/Get):如果返回视图,用户按 F5 刷新会把上一次 POST 请求再发一遍,数据重复插入;重定向到列表页后,刷新只是重新 GET,不会造成重复提交。
还有 @PathVariable("id") 绑定 URL 路径上的 {id},和 @RequestParam 不同,后者取 ?id=1 这种查询参数。用 RESTful 风格时,编辑和删除都走路径参数,经过 URL 编码,比查询参数更干净。
3.3 日期字段和表单回显:类型转换的默认行为
实体类 Student 里 birthday 是 Date 类型。表单提交的是 2024-03-05 这种字符串,SpringMVC 不会自动把字符串转成 java.util.Date,必须指定格式。常见做法是在字段上加注解:
public class Student { private Integer id; private String name; private String gender; @DateTimeFormat(pattern = "yyyy-MM-dd") private Date birthday; private String phone; private String email; }@DateTimeFormat 依赖 mvc:annotation-driven 自动注册的转换器,不用再单独配置。但这里有个隐藏问题:如果前端用的是带时分秒的日期时间控件,提交的值是 2024-03-05 14:30:00,而 pattern 只写了 yyyy-MM-dd,转换直接失败,返回 400。所以 pattern 必须和前端实际的展示格式严格一致。
列表页回显时也有一个类似的问题。JSP 里直接通过 el 表达式输出 Date 对象,得到的是英文格式的一长串时间,比如 Sun Mar 05 14:30:00 CST 2024。解决方法是 JSP 里引入 fmt 标签库,用 fmt:formatDate 统一格式化;或者更省事,在实体类里额外加一个字符串类型的 birthdayStr 字段,只用于表单回显,不映射数据库列,很多老项目都愿意这么干,改动最小。
提示:前端只传日期不传时间,就用 yyyy-MM-dd;传了时间就写 yyyy-MM-dd HH:mm:ss。两种 pattern 不匹配,报错都在提交那一瞬间,浏览器直接给 400。
4. 拦截器与登录校验:给系统加上统一的访问控制
4.1 HandlerInterceptor 实现登录拦截
学生管理系统一般要求登录后才能操作,拦截器是实现这个需求的首选。SpringMVC 里自定义拦截器是实现 HandlerInterceptor 接口,核心方法是 preHandle:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/login"); return false; } return true; } }preHandle 的返回值决定请求是否继续:true 进入 controller;false 请求到此为止,由拦截器自己决定跳转方向。这里用 Session 里有没有 loginUser 判断登录状态,登录成功后把用户信息塞进 Session。相关代码在 LoginController 里:
@Controller public class LoginController { @RequestMapping(value = "/login", method = RequestMethod.GET) public String loginPage() { return "login"; } @RequestMapping(value = "/login", method = RequestMethod.POST) public String doLogin(String username, String password, HttpSession session, Model model) { if ("admin".equals(username) && "123456".equals(password)) { session.setAttribute("loginUser", username); return "redirect:/student/list"; } model.addAttribute("error", "用户名或密码错误"); return "login"; } }这段登录逻辑是写死的演示账号,真实项目里至少要从用户表查密码。但拦截器关心的不是账号密码,而是 Session 里有没有登录标记。登出时调用 session.invalidate() 把整个会话失效,比单独 removeAttribute 彻底。
preHandle 返回 false 后要注意响应是否被提交。如果直接 return false 却不给客户端任何响应,浏览器会一直停在那个请求上直到超时。所以拦截器里要么 sendRedirect,要么 forward,要么给前端返回一段 JSON。老项目里还有一种常见情况:页面发 AJAX 请求被拦截,服务端 sendRedirect 到登录页,前端拿到的是登录页 HTML 而不是 JSON,解析直接报错。处理办法是判断请求头 X-Requested-With,如果是 AJAX,直接返回 401 状态码,由前端统一跳转登录页。
4.2 拦截路径和放行规则:配置顺序是有讲究的
在 springmvc.xml 里注册拦截器:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.demo.student.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>mvc:mapping path="/" 表示拦截所有路径,exclude-mapping 用来排除不需要登录的路径。实际项目里至少还要排除登录的 POST 提交地址,比如 /loginSubmit、/captcha。如果忘了排除 /static/,页面刷新后 CSS 和 JS 会被拦下来,样式全丢,但页面 HTML 正常出来,这种半残现象最容易让人摸不着头脑。
路径匹配用的是 Ant 风格,几个通配符的差别列在下面:
| 配置 | 匹配范围 | 常见用途 |
|---|---|---|
| /login | 精确匹配 /login | 登录页 GET 请求 |
| /login/** | /login 下的所有子路径 | 登录相关的多个接口 |
| /** | 所有请求 | 全局拦截 |
| /static/** | /static 下的静态资源 | 放行 CSS、JS、图片 |
配置顺序上有个经验:mvc:interceptors 和 mvc:resources 没有严格的先后要求,但拦截器的 exclude 一定要写全。否则即使 Spring 把静态资源交给默认处理器,HandlerInterceptor 依然会在进入处理器之前执行,loginUser 为 null 时静态资源请求也会被重定向到登录页。拦截器工作在前端控制器处理链上,它按请求路径匹配,并不关心这个请求最终是不是真的交给 controller。
4.3 一个容易忽略的边界:POST 登录请求的放行
这里单独提一个实际维护中见过多次的情况。很多人配置了 /login 放行,但登录表单的 action 提交到了 /loginCheck,这个路径没有加进 exclude-mapping,结果是点了登录按钮直接跳到登录页,还以为是账号密码错了。排查方法很简单:在浏览器开发者工具里看请求路径,再把 exclude 列表和实际请求地址对着查一遍。这套资源的登录提交路径是 /login 的 POST 方法,所以只排除 /login 就够了,但如果你接手的是别人的老项目,注意确认表单 action 的实际值。
另一个边界是登出路径。登出操作本身需要登录态,所以 /logout 不应该放行,否则登出按钮点击后先被拦截器接到,没走到登出方法就直接被重定向到登录页。从用户角度看,好像也“登出”了,但 Session 里的 loginUser 并没有被清除,如果用户按浏览器后退键,还能回到登录后的页面。这种细节看需求怎么定义,如果要求严格,登出路径应该留在拦截范围内,让登出方法去调 session.invalidate()。
5. 避坑排查:从跑起来到真正上线,五个常见坑的复盘
5.1 容器装配类:Bean 找不到与重复扫描
坑一:启动报 NoSuchBeanDefinitionException,提示 studentService 或 studentDao 找不到。现象是 Tomcat 起来后一访问 /student/list 就报 500,控制台堆栈写着“No qualifying bean of type 'StudentService'”。原因无非两种,第一种是包扫描路径不对,component-scan 的 base-package 没有覆盖到 service 实现类所在的包;第二种是 @Service、@Repository 注解漏写,Spring 不认这个类。解决方法是先看注解,再看包路径,最后把启动日志调到 DEBUG,确认容器到底注册了哪些 Bean。
坑二:同一个 Bean 被扫描两遍,注入点报“expected single matching bean but found 2”。现象是启动过程不报错,运行到某个注入点才报类型冲突。原因是 applicationContext.xml 和 springmvc.xml 都写了全包扫描,service 在父子容器里各有一份。解决方法是父容器扫描时排除 @Controller,子容器关闭默认过滤器只包含 @Controller。这个点在第 2 章就强调过,实际操作里多数“事务不生效”的问题就是这里埋下的。
5.2 请求处理类:静态资源、日期格式、中文乱码
坑三:页面能打开,但 CSS、JS 全部 404。现象是控制台不报错,浏览器 Network 面板里 .css 和 .js 都是红色,状态码 404。原因是 DispatcherServlet 映射了 /,把静态资源请求拦截后当成 controller 处理,而项目里没有对应的处理器。解决方法是配 mvc:resources,并确认 JSP 里引用的路径带了项目上下文:
<mvc:resources mapping="/static/**" location="/static/"/>配完还是 404,就检查 webapp 下是不是真的存在 static 目录,以及 JSP 里的路径是不是写成了 /static/css/style.css 而少了上下文前缀。最保险的写法是统一用 c:url 标签,或者 ${pageContext.request.contextPath}/static/...。
坑四:日期字段提交报 400 Bad Request。现象是表单里填入日期,保存时浏览器直接返回 400,控制台提示“Failed to convert from type [java.lang.String] to type [java.util.Date]”。原因是实体类没有加转换规则,SpringMVC 的默认转换器不认识 yyyy-MM-dd 这种字符串。解决方法是加 @DateTimeFormat(pattern = "yyyy-MM-dd"),并保证 pattern 和前端实际提交格式完全一致。如果前端日期控件带时间,pattern 就要写成 yyyy-MM-dd HH:mm:ss,时间部分解析不了同样报 400。
坑五:中文乱码,入库的数据变成问号。现象是表单提交的中文在列表页显示正常,但数据库里是 ??,或者列表页直接乱码。原因是 web.xml 没有配置 CharacterEncodingFilter,或者配置了但 forceEncoding 没设 true;另一种可能是 jdbc.url 没带 characterEncoding 参数。解决方法是先配过滤器,再检查连接串。配置如下:
<filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这个过滤器要放在 web.xml 过滤器链的第一个位置,如果后面还有其他过滤器,编码过滤器不靠前,别的过滤器可能已经按默认编码读了请求体。jdbc.url 里要追加 useUnicode=true&characterEncoding=utf8,注意 XML 里 & 符号要转义成 &。
5.3 数据一致性类:事务不回滚与重复提交
坑六:方法里抛了 RuntimeException,前面的 insert 没有回滚。这个坑在旧项目里出现频率极高,现象是故意造一个运行期异常,数据库里数据照样多了。顺着三个方向排查基本能定位:
| 排查点 | 检查内容 | 常见结论 |
|---|---|---|
| 注解 | 导入的包是否是 spring 的 @Transactional | 导错包时注解不生效 |
| 容器 | service 是否在父容器扫描范围内 | 子容器扫到 service 时事务代理失效 |
| 调用方式 | 是否同类内部 this.xxx() 自调用 | 自调用不经过代理对象 |
同类自调用这个问题比较隐蔽:AdminService 里 saveStudent 方法调了 this.sendNotice(),sendNotice 上标了 @Transactional,但因为走的是 this 对象,没有经过 Spring 代理,事务不起作用。解决是把需要事务的方法放到另一个 Bean 里,或者用 AopContext.currentProxy() 显式获取代理对象。注意 AopContext 需要 expose-proxy=true 才能用,配置里加一行 <aop:aspectj-autoproxy expose-proxy="true"/>。这个开关老项目不一定开着,所以建议优先用拆分 Bean 的方式。
表单重复提交也常归到这一类。现象是新增成功后刷新一次,列表里多出两条相同记录。原因是新增操作走了 POST,返回的是视图而不是重定向。解决是在新增、修改、删除成功后统一走 redirect,也就是 PRG 模式。这套资源里的保存和删除都是 redirect,直接照抄就不会踩这个坑。如果你是改造老代码,重点检查 controller 返回的字符串是不是以 redirect: 开头。
6. 再进一步:把列表查询封装成可复用的条件查询对象
列表页只做 findAll 够用,但一旦要按姓名、性别筛选,或者做分页,最怕的就是在每个方法里拼 SQL。常见做法是定义一个查询条件对象 StudentQuery,把筛选参数和分页参数收拢在一起:
public class StudentQuery { private String name; private String gender; private Integer pageNo = 1; private Integer pageSize = 10; public String getName() { return name; } public void setName(String name) { this.name = name; } // 其余 getter/setter 省略 }DAO 里根据条件动态拼 where,参数用 List 收集,最后统一转数组:
public List<Student> findByQuery(StudentQuery query) { StringBuilder sql = new StringBuilder( "select id, name, gender, birthday, phone, email from t_student where 1=1 "); List<Object> args = new ArrayList<>(); if (StringUtils.hasText(query.getName())) { sql.append("and name like ? "); args.add("%" + query.getName() + "%"); } if (StringUtils.hasText(query.getGender())) { sql.append("and gender = ? "); args.add(query.getGender()); } sql.append("order by id desc limit ?, ?"); args.add((query.getPageNo() - 1) * query.getPageSize()); args.add(query.getPageSize()); return jdbcTemplate.query(sql.toString(), args.toArray(), new BeanPropertyRowMapper<>(Student.class)); }这段代码有两个边界值得注意。第一,条件全部通过占位符传入,不拼接字符串,杜绝了注入;第二,分页用的 limit 是 MySQL 方言,如果底层换成 Oracle,分页语句要改成 rownum 写法,这套资源的 SQL 是 MySQL 风格,迁移数据库时这里一定要单独处理。
我维护过一段带 CRM 模块的老代码,原来的查询是每个方法单独写一遍 SQL,加了三个筛选条件后,代码里出现六七段大同小异的 where 拼接。后来统一收敛成查询对象结构,Controller 接收查询参数,组装成查询对象,DAO 只根据对象是否有值决定加不加条件。从那以后,我每次接到 SpringMVC 老项目,都会先翻一遍 DAO 层有没有堆砌的字符串拼接,看到就建议改成这种写法,最大的好处是以后加筛选条件时,只改 Query 字段和 DAO 里的一个 if 分支,不去动接口签名。希望帮到你。
本文还有配套的精品资源,点击获取