做了不少JavaWeb方向的毕设和练手项目之后,我越来越觉得SSM这类“老组合”其实才是理解后端开发的绝佳教材。这次以一个律师事务所律师管理系统为例,把SSM(Spring+SpringMVC+MyBatis)配合Maven、JSP、MySQL的完整开发过程拆开讲透,希望给准备做JavaWeb项目或者正在为毕设选题头疼的朋友一些可落地的参考。
先说清楚这个系统能干什么。它本质上是一个典型的企业级信息管理后台:律师信息维护、案件登记跟踪、客户资料管理、法律文书上传下载、开庭提醒、收费记录统计。前端用JSP渲染页面,后端用SSM处理业务逻辑,MySQL存数据,Maven管依赖和构建。适合的人群很明确:正在学JavaWeb的大学生、准备毕设的准毕业生、以及想快速上手SSM整合开发的初级工程师。标题里那些技术词——javaweb、ssm、maven、jsp、mysql——看起来是五个独立的技术点,但在这个项目里它们是一条完整的业务链路,任何一个环节掉链子,整个系统都跑不起来。
1. 项目整体设计与需求拆解
1.1 律所管理系统的核心业务模型
别一上来就写代码,先把业务想清楚。律所管理系统的核心是“人”和“事”的流转。我梳理需求时习惯先画业务主线,再拆数据表,最后才动手建工程。
一个律所管理系统的核心业务主线大概是这样的:客户进门提出咨询需求和接待过程,再到洽谈委托、律所内部指定承办律师;律师接案后,需要把证据材料归档为法律文书;案件推进过程中,要记录每一次的调解、开庭、判决结果,也就是案件进度;最后是收费记录,包括代理费、咨询费和其他费用。
围绕这条主线,用户角色必须分层。管理员负责全局,管理律师账号、审核数据;律师负责处理自己的案件和客户;前台或行政人员负责接待登记和预约安排。这三个角色对应到系统中就是三套不同的功能菜单和数据权限,SSM里用拦截器就能搞定权限控制,按角色放行对应的URL和操作按钮。
1.2 功能模块划分与优先级排序
功能拆解是重头戏。我按“必须要有、最好要有、锦上添花”三档来排需求,避免一上来就把系统设计得又大又空。
第一档核心功能,缺了就不像律所管理系统:律师信息管理(增删改查+状态启停)、案件管理(新增委托、分派律师、进度状态流转)、客户管理(登记、联系人、历史委托记录)、用户登录和权限控制。
第二档进阶功能,提升系统可用性:预约管理(来访预约、回访计划)、收费记录(按案件和客户两条维度记录金额)、法律文书管理(上传下载、按案件归档)。
第三档锦上添花:数据统计看板(案件数量、收费总额、律师办案量排名)、全文检索引擎、消息提醒。
实际项目里我给客户的方案是:第一档必须全做完,第二档挑两到三个核心项,第三档看时间。别贪大,一个能完整跑通核心闭环的系统,比十个只做了半截的功能模块值钱得多。
1.3 SSM框架组合在项目中的角色划分
SSM三个框架的分工可以用一句话说清:Spring管对象、SpringMVC管请求、MyBatis管数据库。听起来简单,但它们在代码里的职责边界一旦模糊,后期维护就是灾难。
Spring在项目里是“总装配车间”。所有Service、Dao、事务管理器、数据源、拦截器这些Bean,全部交给Spring容器创建和注入。配置可以用XML,也可以用注解加配置类。我后来的习惯是:全局配置用XML(比如数据源、事务),业务Bean用注解扫描。
SpringMVC是“前台接待”。用户发来的每一次HTTP请求,都由DispatcherServlet统一接收,HandlerMapping找到对应的Controller方法,HandlerAdapter调用业务逻辑,最后ViewResolver解析JSP页面返回给浏览器。注意,Controller层做的是参数接收、调用Service、组装ModelAndView,绝对不能写SQL和业务规则。
MyBatis是“数据搬运工”。Mapper接口定义方法,XML文件写SQL,两者通过命名空间和方法名绑定。什么时候要延迟加载、什么时候用动态SQL拼条件、resultMap怎么映射驼峰字段,这些都在MyBatis层解决。SQL写得好不好,直接决定系统在大数据量下的表现。
2. 技术选型背后的思考
2.1 为什么选SSM而不直接上Spring Boot
现在做JavaWeb新项目,大多数人会直接选Spring Boot。但在这个律所管理系统项目里,我坚持用传统SSM,原因有三。
第一,学校课程和很多毕设选题的技术要求就写了“基于SSM框架”,换技术栈等于主动违规。第二,SSM需要手写大量XML配置,这个过程恰恰能让你理解Spring IoC的加载时机、MyBatis的Mapper绑定原理、SpringMVC的九大组件分别在哪配置、干活的顺序是什么。说白了,Spring Boot帮你隐藏的细节,在SSM里全都要自己面对,跑通一遍之后,你对整个JavaWeb请求响应链路会有肌肉记忆。第三,SSM项目打包成WAR丢进Tomcat就能跑,部署方式直白,对毕设答辩的演示环境更友好。
但我必须提醒一句:如果是纯生产项目,没有既定的教学或毕设约束,直接用Spring Boot + MyBatis Plus确实更高效。技术选型从来没有绝对的对错,只有适不适合当前场景。你选SSM就要想清楚是用它来学习底层原理、满足课程要求,而不是因为它“更先进”。
2.2 Maven在项目里的真实价值
Maven在这个项目里的角色是“包管理员”和“构建总管”。没有Maven的时候,你要手动去网站下载Spring的jar包、MyBatis的jar包、mysql驱动、Jackson、JSTL标签库,然后一个个复制到WEB-INF/lib目录。版本冲突、缺失依赖是常态,换个环境整个项目直接编译失败。
Maven用pom.xml声明依赖坐标,.m2本地仓库做缓存,中央仓库做下载源。我这里建议你在Maven的settings.xml里配置阿里云镜像,原因不用多说,中央仓库在国内的下载速度会让你怀疑人生。配置方式是在mirrors节点加一段:
<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>mirrorOf填*表示所有仓库请求都走这个镜像,实测下载速度提升非常明显。
除了依赖管理,Maven的另一个核心价值是“约定大于配置”。标准目录结构是src/main/java、src/main/resources、src/webapp,多模块拆分、打包WAR、执行测试、一键clean install,全都是Maven的命令行能力。项目交接给别人的时候,只要有pom.xml和源码,一条mvn clean install就能把环境拉起来,不会再出现“在我电脑上是好的啊”这种史诗级尴尬。
2.3 数据库连接池为什么必须加
JDBC直连数据库的方式性能太差,每次请求都要经历建立连接、校验身份、TCP握手、断开连接,高并发下连接数是灾难性的。连接池的核心思想就一句话:提前创建一批数据库连接放在池子里,谁用谁取,用完归还。
SSM项目里我用的是Alibaba的Druid。配置Druid后,依赖里加druid-spring-boot-starter(如果是SSM就加druid的普通依赖再加配置类),然后在Spring配置文件里把数据源替换成DruidDataSource。核心配置项有这些:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/law_firm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456 jdbc.initialSize=5 jdbc.minIdle=5 jdbc.maxActive=20 jdbc.maxWait=60000initialSize是启动时初始化的连接数,maxActive是最大活跃连接数,maxWait是拿不到连接时的最长等待毫秒数。生产环境建议把maxActive设置在20到50之间,太小容易排队,太大浪费数据库资源。Druid最实用的能力是监控页面,在web.xml里配置一个StatViewServlet,访问/druid/index.html就能实时看到SQL执行次数、并发连接数、慢查询记录,排查性能问题非常有帮助。
2.4 前端选用JSP的取舍
JSP在2024年看起来确实“复古”,但它在这个项目里有一个其他方案代替不了的优势:服务端渲染。律所管理系统的使用场景是律所内网或者云服务器部署,使用者是行政人员和律师,数据实时性要求高,页面不需要复杂的客户端交互。
JSP+EL+JSTL是我推荐的组合。EL表达式写${lawyer.name}取出后台传来的数据,JSTL的<c:forEach>循环渲染列表,<c:if>控制按钮显示权限,基本不用写一行Java代码。在JSP页面里,<%@ page contentType="text/html;charset=UTF-8" language="java" %>和<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>这两个指令是标配,前者声明页面编码防中文乱码,后者引入核心标签库。
有人会问,JSP和前端分离方案相比是不是落后了。实话实说,如果做面向C端用户的互联网产品,我肯定推荐Vue+Axios+后端JSON接口。但做内部管理系统,JSP还是有它独特的价值:开发效率高、SEO天然友好、权限控制直接在页面标签层级完成。适合的才是最好的。
3. 数据库设计与核心实现细节
3.1 数据库表结构设计的思路
表设计遵循“一业务一表,一关系一表”的原则。律所系统我规划了至少八张核心表,这里介绍最关键的五张:
- t_lawyer(律师表):id、lawyer_no(工号)、name、gender、phone、title(职称,如合伙人/执业律师)、specialty(擅长领域)、status(在职/离职/休假)、create_time。唯一索引建在lawyer_no上,避免工号重复。
- t_case(案件表):id、case_no(案号)、title(案件名称)、case_type(案件类型,如民事/刑事/非诉)、lawyer_id(承办律师外键)、client_id(客户外键)、status(待分配/进行中/已结案/已归档)、accept_time、close_time。
- t_client(客户表):id、name、phone、email、company、source(获客渠道)、create_time。这里客户和律师是多对多关系,一个客户可以委托多个律师,一个律师也可以服务多个客户,通过关联表t_client_lawyer维护。
- t_appointment(预约/接待表):id、client_id、lawyer_id、appointment_time、purpose(预约事由)、feedback(接待反馈)、status(待接待/已接待/已取消)。
- t_fee(收费记录表):id、case_id、fee_type(服务费/咨询费/差旅费)、amount(金额)、pay_time、collector_id(收款人)、remark。金额字段用DECIMAL(10,2),别用FLOAT和DOUBLE,精度坑会让你后面想骂人。
所有表统一带上create_time、update_time、is_deleted三个“通用字段”。逻辑删除比物理删除安全,查数据时加条件是where is_deleted = 0。
3.2 数据库设计时的常见坑
第一坑:数据库表名用复数。MySQL在Linux服务器上区分大小写,Person和person是完全不同的表。我的惯例是全表用小写加下划线,比如t_lawyer、t_case,避免各种镜像环境下的兼容性问题。
第二坑:外键约束。学生项目里我见过到处写FOREIGN KEY的,结果删数据的时候被外键挡住,一删一个不吱声。我的建议是逻辑上保留关联关系,物理上不建外键约束。关联查询用JOIN,删除用程序控制,这样既灵活又不会因为外键报错影响演示。
第三坑:字符集排序规则。建表时统一用utf8mb4,别用utf8。utf8mb4是真正的四字节UTF-8,能存表情符号和生僻字。排序规则选utf8mb4_general_ci,实测性能更好。
第四坑:索引滥用。给每个字段都加索引,看着很美,写入性能会直线下降。遵循最左前缀原则:where后面的第一个条件字段建立索引,关联字段建立索引,其他按需建立。比如t_case表里,lawyer_id和client_id是外键关联字段,必需索引;case_type选择性太低(只有几种固定值),不需要单独索引。
3.3 MyBatis核心SQL编写要点
MyBatis的精髓在Mapper XML文件。基础增删改查用自动生成的方法就行,但业务报表类的统计SQL,就要手动写了。
案件状态统计是系统里最典型的一段SQL,查每个律师名下的在办案件数和已结案数:
SELECT l.name AS lawyer_name, COUNT(CASE WHEN c.status = '进行中' THEN 1 END) AS handling_count, COUNT(CASE WHEN c.status = '已结案' THEN 1 END) AS finished_count FROM t_lawyer l LEFT JOIN t_case c ON l.id = c.lawyer_id WHERE l.status = '在职' GROUP BY l.id ORDER BY handling_count DESC这段SQL的LEFT JOIN是关键,LEFT JOIN保证了即使某个律师一个案子都没有,也会查出一条记录,COUNT里用CASE...WHEN做有条件计数。很多人会在这里写WHERE c.status = '进行中',那就完蛋了,逻辑上变成只统计有进行中案件的律师,没案件的律师直接过滤掉了。
分页查询在SSM项目里用PageHelper插件。Service层代码只需要在查询前调用一行PageHelper.startPage(pageNum, pageSize),然后正常写List查询,PageHelper会在底层自动拦截SQL并生成count和limit语句。分页返回的结果用PageInfo包装,pageInfo.getList()拿当前页数据,pageInfo.getTotal()拿总记录数,pageInfo.getPages()拿总页数。这里有个坑:PageHelper.startPage必须紧挨着你要分页的那条Mapper查询方法,中间如果隔了别的查询语句,分页会不生效。
4. 完整实操过程与核心环节实现
4.1 开发环境准备与IDEA配置
这是最容易劝退新手的环节,我尽量把步骤写细。
第一步:安装JDK 8或11。JDK版本别搞太高,JDK 17用老版本Tomcat和MyBatis会有兼容问题。配置JAVA_HOME环境变量,path里加%JAVA_HOME%\bin,命令行里输入java -version验证。
第二步:安装Maven 3.6.3或3.8.x。解压到纯英文路径(C:\maven),MAVEN_HOME配好,验证mvn -v。改settings.xml里的本地仓库路径和阿里云镜像,本地仓库路径最好也放一个不包含中文和空格的目录,比如D:\maven-repo。
第三步:安装MySQL 5.7或8.0。注意8.0的驱动类名是com.mysql.cj.jdbc.Driver,5.7是com.mysql.jdbc.Driver,别用错。安装时选utf8mb4字符集,密码记牢。安装完用Navicat或命令行建库:CREATE DATABASE law_firm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。
第四步:IDEA配置。新建Maven项目时,在IDEA的Build Tools -> Maven设置里,把Maven home path指向你本地解压的Maven目录,User settings file指向你改过的settings.xml,Local repository会自动识别。这个步骤不做,IDEA默认用内置Maven和C盘仓库,速度慢还容易和命令行版本不一致。
第五步:配置Tomcat。在IDEA里Add Configuration -> Tomcat Server -> Local,配置Tomcat路径为本地解压目录。Deployment里添加Artifact,选择xxx:war exploded模式(开发用exploded,热部署快),Application context填/,访问路径就是http://localhost:8080/。
4.2 SSM工程结构与管理依赖
标准的SSM项目目录结构大概是这样的:
law-firm-management/ ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ ├── com.lawfirm │ │ │ │ ├── controller │ │ │ │ ├── service │ │ │ │ ├── dao │ │ │ │ ├── entity │ │ │ │ ├── interceptor │ │ │ │ └── common │ │ ├── resources │ │ │ ├── jdbc.properties │ │ │ ├── spring-mvc.xml │ │ │ ├── spring-mybatis.xml │ │ │ └── mybatis-config.xml │ │ └── webapp │ │ ├── WEB-INF │ │ │ ├── web.xml │ │ │ └── jsp │ │ ├── static │ │ └── index.jsppom.xml里最核心的依赖有:spring-webmvc(统一管理Spring全家桶)、mybatis和mybatis-spring(MyBatis与Spring整合)、mysql-connector-java(数据库驱动)、druid(连接池)、jstl(JSP标签库)、jackson-databind(JSON序列化)。Spring版本建议用5.x,搭配JDK8非常稳。总依赖数量控制在20个以内,别没事瞎加依赖,jar包冲突排查起来很头大。
4.3 Spring与MyBatis整合配置
spring-mybatis.xml是SSM项目里最重要的一份配置,核心内容如下:
<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.lawfirm.entity"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.lawfirm.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>关键点逐个说。context:property-placeholder负责加载jdbc.properties,把配置外置,换环境改配置就行,不用重新编译。mapperLocations告诉MyBatis去哪找SQL映射XML文件,路径要写成classpath:mapper/*.xml。typeAliasesPackage配置实体类所在包,Mapper XML里写resultType可以直接用类名(如com.lawfirm.entity.Lawyer),否则要写全限定名。MapperScannerConfigurer会自动扫描dao包下的所有接口,生成代理对象注入Spring容器。事务管理器配上@Transactional注解,Service层的数据库操作就自动进入事务保护,任何一步抛异常整体回滚。
4.4 SpringMVC核心配置
spring-mvc.xml相对简单,核心是三个:扫描Controller、开启注解驱动、配置视图解析器。
<context:component-scan base-package="com.lawfirm.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/>视图解析器的作用是逻辑视图名到物理JSP路径的映射,Controller里return "lawyer/list",实际找的是/WEB-INF/jsp/lawyer/list.jsp。把JSP放在WEB-INF下是有讲究的,外部浏览器不能直接通过URL访问WEB-INF目录,只能由Controller转发进来,多了一层安全防护。
web.xml里要配置两个核心内容,一个是Spring的ContextLoaderListener监听器,加载Spring容器(spring-mybatis.xml),另一个是SpringMVC的DispatcherServlet,拦截所有以.do结尾或所有请求。有个细节:如果DispatcherServlet配置的是/,会拦截所有请求,SpringMVC会优先匹配HandlerMapping,匹配不上的请求才交给容器默认Servlet,这可能导致静态资源404,配合上面的<mvc:resources>配置来放行静态资源。
4.5 登录、权限拦截与案件列表实现
登录逻辑是所有后台系统的命门。用户密码存储绝对不能明文,我用MD5加盐的方式。注册或创建账号时,把用户输入的密码加盐后做MD5,保存到数据库;登录时用同样的盐和算法生成摘要,和库里存的比对。加盐是核心,加盐值可以用用户名拼一个固定的salt字符串,定期更换salt可以提升安全性,但代价是所有用户都得重置密码。
登录成功后,把用户对象放进session,同时把登录状态写入一个在线用户标记。权限控制用SpringMVC的HandlerInterceptor实现:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }在spring-mvc.xml里注册拦截器:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/login.jsp"/> <mvc:exclude-mapping path="/static/**"/> <mvc:interceptor> <bean class="com.lawfirm.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptor> </mvc:interceptors>登录接口的处理逻辑是:Controller接收username和password,Service层查库比对,成功则存session返回成功JSON,前端用JavaScript跳转主页面;失败则返回错误信息。
案件列表配合PageHelper做分页是最典型的功能。Controller方法:
@RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); List<Case> caseList = caseService.findAll(); PageInfo<Case> pageInfo = new PageInfo<>(caseList); model.addAttribute("pageInfo", pageInfo); return "case/list"; }JSP页面里循环渲染加翻页按钮:
<c:forEach items="${pageInfo.list}" var="c"> <tr> <td>${c.caseNo}</td> <td>${c.title}</td> <td>${c.lawyerName}</td> <td>${c.status}</td> </tr> </c:forEach>这个功能跑通之后,整个系统的“读数据-传数据-展示数据”链路就跑通了,其他业务模块照葫芦画瓢就行。
5. 高频问题排查技巧与避坑实录
5.1 数据库连接报错怎么定位
这类问题在项目演示前出现率极高,我按现象分一下类:
第一类:Access denied for user 'root'@'localhost'。这是用户名密码错误,或者MySQL的认证插件升级导致密码策略不匹配。MySQL 8默认用caching_sha2_password,老驱动不支持,解决方式有两种:一是在连接串上加allowPublicKeyRetrieval=true,二是把MySQL用户认证插件改回mysql_native_password:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';。
第二类:Communications link failure。常见原因是MySQL服务没启动(Windows上没安装成服务,需要在MySQL安装目录bin下执行mysqld --install)、端口不是3306(被其他程序占了)、url里localhost解析成了IPv6的::1(改成127.0.0.1)。
第三类:Unknown database 'law_firm'。纯粹是数据库没建,或者url里的库名和实际库名不一致。IDE里配置数据源时一定要确认URL、用户名、密码三项和实际环境完全一致。
第四类:时区报错。连接串后面一定要加serverTimezone=Asia/Shanghai,不加的话MySQL 8高版本会要求你设置serverTimezone,否则启动即报错。
5.2 Maven依赖与编译问题
最常见的是Failed to read artifact descriptor for xxx:jar,这是本地仓库的jar包损坏或者没有对应版本。我的排查路径是:打开IDEA的Maven窗口,执行clean,然后reimport,如果还不行,去本地仓库把对应目录整个删掉,重新让Maven下载。
Cannot resolve symbol 'SpringApplication'这类问题通常是idea的缓存问题。Maven Projects面板里点一下刷新按钮,让IDE重新加载pom.xml。如果还是没有,检查一下Dependencies里是否显示红色的波浪线或者missing状态。
编译期报程序包不存在,十有八九也是依赖没下全。命令行执行mvn clean install -U,-U参数会强制检查远程仓库更新。如果网络不稳定,就把阿里云镜像的mirrorOf改成central而不是*,只对中央仓库生效,别的仓库走原始地址。
5.3 JSP页面报错与404问题
JSP页面报500错误,先看Tomcat的catalina.out日志,控制台输出乱码先解决编码问题。Tomcat的conf目录下logging.properties里有一行java.util.logging.ConsoleHandler.encoding = UTF-8,默认是UTF-8,如果你用Windows看到乱码,改成GBK再重启。
404问题排查思路从外向内:先在浏览器直接访问Tomcat根路径,能出Tomcat默认页面说明Tomcat活着;再访问部署的应用名,404说明WAR包没加载成功;打开IDEA的Tomcat配置,Deployment里确认Artifact已经添加,Application context是否正确。
最经典的坑是JSP页面用了<%@ page isELIgnored="true" %>,或者web.xml里配置了较新版本的Servlet规范,让EL表达式默认关闭,导致页面上的${}全部原样输出。解决办法是把web.xml的web-app版本声明为3.0或更高,或者在JSP页面明确设置isELIgnored="false"。
5.4 事务失效的隐蔽原因
Service方法加了@Transactional却不生效,回滚不了,这是SSM项目里比较隐蔽的坑。排查顺序务必要记牢:
第一,确认@Service注解在类上,并且类被Spring扫描到了。注意@ComponentScan扫描的basePackage有没有覆盖到这个Service包,扫描不到,谈什么事务。
第二,确认@Transactional是放在类名或public方法上。放在private方法上不生效,因为Spring的AOP默认用CGLIB或JDK动态代理,私有方法根本不会进入代理逻辑。
第三,确认事务管理器被正确配置,并且在spring-mybatis.xml里声明了<tx:annotation-driven>。
第四,确认你没有在同一个类里“自调用”。比如ClassA中的methodA直接调用同一个类的methodB,而methodB加了@Transactional,此时事务不会生效,因为方法调用走的是this.methodB,没有经过代理对象。解决办法是把methodB拆到另一个Service类里,或者通过注入自身代理实现调用。
5.5 前后端联调时的常见场景
系统里凡是涉及到日期、金额的类型转换,特别容易出幺蛾子。前端传字符串日期到Controller,SpringMVC默认不能直接转成Date,需要在Controller里用@DateTimeFormat(pattern = "yyyy-MM-dd")注解,或者配置全局转换器。金额用字符串传到后台,可以考虑用BigDecimal类型接收,避免浮点损失。
JSON序列化循环引用也常见,比如用户A关联了案件B,案件B又关联了用户A,Jackson序列化时可能出现无限递归。解决方式是在关联字段上加@JsonIgnore注解,或者在实体类的@JsonIdentityInfo上配置统一处理。这种问题在项目答辩演示时最尴尬,一定要提前测。
6. 系统的可扩展方向
SSM这套架子打通之后,扩展性是肉眼可见的。我列出几个实用的方向,按性价比排序。
方向一:引入Redis做用户会话和热点数据缓存。登录状态存Redis、律师简介和案件类型字典数据缓存,能显著降低MySQL的查询压力。只需要加一个spring-data-redis依赖和RedisTemplate配置,改动成本不大。
方向二:补充运营看板能力。把统计SQL做成定时任务,每天固定时间把数据汇总到统计表,前端展示图表。此处要注意,连接池的监控指标可以直接对接Druid的StatFilter,跑批任务建议把任务间隔控制在低峰时段。
方向三:业务数据导出Excel。用Apache POI或者阿里EasyExcel,把案件列表、收费统计直接导出成Excel报表。EasyExcel的API比POI温和得多,几行代码就能实现复杂表头,对律所行政人员很实用。
方向四:微服务拆分。律所系统如果用户量和业务复杂度继续膨胀,可以把“案件管理、客户关系、收费财务、文书归档”四个核心模块拆成独立的Spring Boot服务,用Nacos做注册中心,Feign做远程调用,Gateway做网关,SSM项目里的Service拆分思路可以直接迁移过去。不过一个律所管理系统的用户规模大概率到不了这一步,拆不拆看实际情况,不要为了微服务而微服务。
最后分享一点个人感受:做一个SSM项目最忌讳的就是把五个技术栈分开学、分开用。真正跑通一个完整的业务闭环,从JSP页面发请求、SpringMVC收到请求调Service、Service里开事务查MyBatis、MyBatis执行SQL返回结果、控制层把数据塞进Model、JSP再渲染出来,整个过程走完一遍,你才会发现JavaWeb面试题里那些“Spring IoC原理”、“MyBatis二级缓存”、“MVC执行流程”其实都是你自己代码里的日常。这个律所管理系统本身不复杂,但它在教学和求职准备上的价值,远远超过一个“毕设项目”的定位。