SSM广告管理系统实战:从环境搭建到AOP日志的完整避坑指南
简介:这份资源是面向高校计算机相关专业学生与Java初学者的一套毕业设计/课程设计完整项目,选题为基于SSM框架的广告管理系统,采用Java语言、SSM+Vue技术栈与B/S架构,数据库使用MySQL,适合需要完成毕设或课程作业、想参考前后端分离实现思路的读者。压缩包共540个文件,约83.42MB,其中135个java文件承载后端业务逻辑,50个vue文件构成前端页面,另有23个xml、19个js及svg、jpg、png等静态资源与2个sql脚本,覆盖配置、界面与建库各环节。系统划分管理员、广告客户、广告商、广告制作商四类角色,功能涉及广告投放渠道、广告需求、广告提案、设计预览、调整信息、签署与结算等模块,业务链条较为完整。目前已有150人学习下载,可帮助读者快速理解多角色权限设计与广告业务流转,并对照源码梳理SSM整合、Vue页面组织与数据库表结构,为二次开发或答辩准备提供参考。
1. 从一份 .rar 说起:SSM 广告管理系统到底在解决什么问题
如果你正在搜「Java毕业设计」或者「SSM框架 广告管理系统」,大概率你手里已经有一个.rar压缩包,或者正准备从零做一个能过答辩、能写进简历、能跑起来的广告投放后台。广告管理系统听起来像大厂才用的东西,但落到毕业设计这个场景,它的本质其实很朴素:把「广告位、广告内容、投放计划、点击统计」这几张表管起来,让运营能增删改查、能按条件筛选、能看到一条广告从创建到上线的完整状态流转。
SSM 就是 Spring + SpringMVC + MyBatis 这套经典组合,虽然现在新项目更多用 SpringBoot,但大量高校课程设计、课程设计案例源码、java课程设计案例源码仍然以 SSM 为标准模板,因为它的分层清晰、配置文件多、能体现你对 IOC、AOP、事务、MVC 这些概念的理解,答辩老师也熟悉这套东西。这个系统适合谁?适合 java基础已经过了一遍、能写简单 CRUD、但还没独立搭过一个完整 Web 项目的同学;也适合工作一两年、想回头补一补传统 SSM 分层和 MyBatis 映射细节的人。
我见过太多人拿到压缩包后直接双击运行,结果 Tomcat 起不来、数据库连不上、页面 404,然后就卡住了。这篇不打算给你灌 java面试八股文,也不聊 java学习路线,就顺着这个标题,把「怎么让这套 SSM 广告管理系统在你机器上真正跑起来、跑对、跑稳」讲清楚,顺带把几个最容易翻车的地方提前给你标出来。
2. 环境与依赖:让 SSM 广告管理系统在本地跑起来的第一步
2.1 先确认你的 JDK、Tomcat、Maven 版本组合
SSM 项目对版本比 SpringBoot 敏感得多,尤其是 JDK 和 Tomcat。很多压缩包里用的是 JDK 8 + Tomcat 8.5 + Maven 3.6 这套组合,如果你本地装的是 JDK 17,编译时大概率会看到java: 警告: 源发行版 17 需要目标发行版 17这类提示,或者更直接的Unsupported class file major version。这不是代码问题,是版本没对齐。
我一般会先做三件事:确认java -version输出是 1.8;确认mvn -v里 JDK 也是 1.8;确认 Tomcat 是 8.5 或 9.0,不要用 Tomcat 10,因为 Tomcat 10 把javax.servlet换成了jakarta.servlet,SSM 老项目里的web.xml和依赖会直接不认。
# 检查 JDK 版本,必须是 1.8 java -version # 检查 Maven 用的 JDK,如果和上面不一致,需要改 JAVA_HOME mvn -v # 检查 Tomcat 版本,建议 8.5.x 或 9.0.x # 进入 Tomcat 的 bin 目录后执行 ./version.sh # Linux/Mac version.bat # Windows逻辑说明:java -version看的是系统默认 JDK,mvn -v看的是 Maven 实际调用的 JDK,两者不一致时以 Maven 为准,因为编译是 Maven 触发的。参数上,JAVA_HOME指向 JDK 8 的安装目录,PATH里把%JAVA_HOME%\bin放在最前面,避免系统里多个 JDK 互相抢。
提示:如果压缩包里带了
.idea或.settings目录,先删掉再导入,否则 IDE 会沿用别人的 JDK 和 Tomcat 路径,你改半天都改不对。
2.2 数据库建库建表:别直接拿 .sql 一把梭
广告管理系统的表通常不多,常见的是ad_user、ad_position、ad_content、ad_plan、ad_stat这几张。压缩包里一般会有一个db.sql或者ad.sql,但直接导入经常报错,原因是字符集和引擎不匹配。
我一般会先手动建库,指定utf8mb4,再导入表结构:
-- 建库,字符集用 utf8mb4,排序规则用通用型 CREATE DATABASE ad_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 切到该库 USE ad_system; -- 再执行压缩包里的建表语句 -- 注意:如果原 sql 里有 DROP TABLE IF EXISTS,先确认没有同名库逻辑说明:utf8mb4比utf8多支持 emoji 和部分生僻字,广告内容里如果出现特殊符号不会乱码。排序规则用utf8mb4_general_ci而不是unicode_ci,是因为老项目里的外键和索引在general_ci下兼容性更好。参数上,如果原 sql 里写的是ENGINE=InnoDB就保留,不要改成 MyISAM,因为后面事务和行锁都依赖 InnoDB。
导入后执行SHOW TABLES;确认表都在,再执行SELECT COUNT(*) FROM ad_position;看有没有初始数据。没有初始数据的话,登录页面可能进不去,因为用户表是空的。
2.3 改配置文件:jdbc.properties 和 applicationContext.xml 的对应关系
SSM 项目里数据库连接通常写在jdbc.properties,然后在applicationContext.xml里用<context:property-placeholder>加载。常见翻车点是路径写错或者 key 名对不上。
# jdbc.properties 示例 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ad_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码逻辑说明:com.mysql.cj.jdbc.Driver是 MySQL 8 的驱动类,如果你用的是 MySQL 5.7,可以改成com.mysql.jdbc.Driver,但建议统一用 8 的驱动加serverTimezone,否则会报时区错误。characterEncoding=utf8和建库时的utf8mb4不冲突,驱动层会自动处理。参数上,serverTimezone=Asia/Shanghai必须加,不然插入时间会差 8 小时。
然后在applicationContext.xml里确认:
<!-- 加载 jdbc.properties --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 数据源,用 Druid 或 DBCP 都行,看压缩包里带的哪个 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <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>逻辑说明:${jdbc.driver}这种占位符必须和jdbc.properties里的 key 完全一致,大小写敏感。如果启动时报Could not resolve placeholder,就是 key 名写错了或者文件没被加载到。参数上,Druid 的initialSize、maxActive可以用默认值,毕业设计并发不高,不用调。
3. 分层与映射:广告管理系统里 MyBatis 和 Spring 怎么配合
3.1 从 Controller 到 Mapper:一次广告查询的完整链路
SSM 的分层是 Controller → Service → Mapper → 数据库。广告管理系统里最典型的一条链路是「按广告位查广告列表」。Controller 接收前端传来的positionId,Service 做业务判断,Mapper 执行 SQL。
// AdController.java @Controller @RequestMapping("/ad") public class AdController { @Autowired private AdService adService; @RequestMapping("/list") public String list(@RequestParam("positionId") Integer positionId, Model model) { // 调用 Service 查广告列表 List<AdContent> ads = adService.getAdsByPosition(positionId); model.addAttribute("ads", ads); return "ad_list"; } }逻辑说明:@Controller和@RequestMapping是 SpringMVC 的注解,@Autowired注入 Service。@RequestParam绑定前端参数,如果前端没传会报 400。参数上,positionId用Integer而不是int,是为了能接收 null 并做后续判断。
// AdServiceImpl.java @Service public class AdServiceImpl implements AdService { @Autowired private AdContentMapper adContentMapper; @Override public List<AdContent> getAdsByPosition(Integer positionId) { // 简单业务判断:positionId 为空时返回空列表 if (positionId == null) { return new ArrayList<>(); } return adContentMapper.selectByPositionId(positionId); } }逻辑说明:@Service标记业务层,@Autowired注入 Mapper。这里加了一个空判断,避免 Mapper 收到 null 后拼出WHERE position_id = null这种查不到任何数据的 SQL。参数上,返回空列表而不是 null,是为了让前端遍历时不会空指针。
<!-- AdContentMapper.xml --> <select id="selectByPositionId" resultType="com.ad.entity.AdContent"> SELECT id, title, img_url, link_url, position_id, status, create_time FROM ad_content WHERE position_id = #{positionId} AND status = 1 ORDER BY create_time DESC </select>逻辑说明:#{positionId}是预编译占位符,能防 SQL 注入。status = 1表示只查上线的广告。resultType里的实体类路径必须和项目里一致,否则报ClassNotFoundException。参数上,ORDER BY create_time DESC让最新的广告排在前面,符合运营习惯。
3.2 事务配置:广告投放和统计更新必须一起成功
广告管理系统里有一个典型场景:用户点击广告后,要同时写点击记录和更新广告的点击总数。这两步必须在一个事务里,否则会出现「记录写了但总数没加」或者反过来。
<!-- applicationContext.xml 里开启事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>逻辑说明:<tx:annotation-driven>让@Transactional注解生效,transactionManager绑定数据源。参数上,dataSource必须和前面配的数据源是同一个 bean,否则事务不生效。
// AdStatServiceImpl.java @Service public class AdStatServiceImpl implements AdStatService { @Autowired private AdStatMapper adStatMapper; @Autowired private AdContentMapper adContentMapper; @Override @Transactional(rollbackFor = Exception.class) public void recordClick(Integer adId) { // 第一步:插入点击记录 adStatMapper.insertClick(adId); // 第二步:更新广告点击总数 adContentMapper.incrementClickCount(adId); } }逻辑说明:@Transactional(rollbackFor = Exception.class)表示任何异常都回滚,默认只回滚运行时异常,受检异常不回滚,所以显式写上更安全。参数上,rollbackFor用Exception.class而不是RuntimeException.class,是为了覆盖所有异常场景。
注意:如果你在 Service 里自己 try-catch 了异常然后没往外抛,事务不会回滚。这是最常见的「事务失效」原因,血泪经验。
4. 避坑与排查:SSM 广告管理系统最常见的 5 个翻车现场
4.1 启动报错NoClassDefFoundError或ClassNotFoundException
现象:Tomcat 启动时控制台刷一堆NoClassDefFoundError,或者访问页面时报ClassNotFoundException,提示某个类找不到。
原因:Maven 依赖没下载全,或者pom.xml里 scope 写成了provided但 Tomcat 里没有对应的 jar。SSM 项目里常见的是spring-webmvc、mybatis-spring、mysql-connector-java这几个包缺失。
解决:在项目根目录执行mvn clean package -DskipTests,看 BUILD 是否成功。如果失败,看具体是哪个依赖下载不下来,换阿里云镜像。如果成功但 Tomcat 还报错,检查WEB-INF/lib下有没有对应的 jar,没有的话在 IDEA 的 Project Structure 里把 Maven 依赖加到 Artifact 里。
4.2 页面 404:Controller 没被扫描到
现象:Tomcat 起来了,访问http://localhost:8080/ad/list返回 404,但控制台没有明显报错。
原因:SpringMVC 的组件扫描路径没覆盖到 Controller 所在的包,或者web.xml里DispatcherServlet的contextConfigLocation配错了。
解决:检查spring-mvc.xml里<context:component-scan base-package="com.ad.controller"/>的包名是否和实际目录一致。再检查web.xml里<init-param>的contextConfigLocation是否指向了classpath:spring-mvc.xml。如果用了classpath*:和classpath:混用,也可能导致加载不到。
4.3 数据库连接池报Too many connections
现象:运行一段时间后,页面卡死,日志里出现Too many connections或者Communications link failure。
原因:Druid 或 DBCP 的maxActive设得太大,或者代码里有地方拿了连接没关。SSM 里 MyBatis 和 Spring 整合后连接由 Spring 管理,一般不会泄漏,但如果你在 Service 里手动getConnection()没关,就会泄漏。
解决:把maxActive调到 20 以内,毕业设计够用。再检查代码里有没有手动操作Connection的地方,有的话改成用 Mapper。另外,MySQL 的wait_timeout默认 8 小时,连接池的minEvictableIdleTimeMillis要小于它,避免拿到失效连接。
4.4 中文乱码:广告标题存进去变成问号
现象:在页面上输入中文广告标题,保存后数据库里显示???,或者页面回显乱码。
原因:三个环节都可能出问题——数据库字符集、连接 URL 字符集、Tomcat 的URIEncoding。常见的是数据库建库时用了latin1,或者连接 URL 没加characterEncoding=utf8。
解决:先确认数据库和表的字符集是utf8mb4,再确认jdbc.url里有characterEncoding=utf8,最后在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。三个都对了,中文就不会乱。
4.5 事务不回滚:点击记录写了但总数没加
现象:调用记录点击的接口后,ad_stat表有新记录,但ad_content的click_count没变。
原因:@Transactional没生效,或者异常被 catch 了没抛出去,或者事务管理器绑定的数据源和 Mapper 用的不是同一个。
解决:先确认applicationContext.xml里<tx:annotation-driven>和transactionManager都配了。再确认 Service 类是被 Spring 管理的(有@Service),而不是自己new出来的。最后确认没有在 Service 里 try-catch 后吞掉异常。如果用的是多数据源,事务管理器要指定对应的数据源。
5. 进阶技巧:用 AOP 给广告管理系统加一层操作日志
5.1 为什么用 AOP 而不是在每个 Service 里写日志
广告管理系统里,运营会频繁地新增、修改、删除广告。如果每个方法里都写一遍log.info("用户{}修改了广告{}", userId, adId),代码会非常臃肿,而且容易漏。AOP 的思路是:定义一个切点,匹配所有 Service 的增删改方法,在方法执行后自动记录操作人和操作内容。
// LogAspect.java @Aspect @Component public class LogAspect { @Autowired private AdLogMapper adLogMapper; // 切点:匹配 com.ad.service 包下所有以 add、update、delete 开头的方法 @Pointcut("execution(* com.ad.service.*.add*(..)) || " + "execution(* com.ad.service.*.update*(..)) || " + "execution(* com.ad.service.*.delete*(..))") public void logPointcut() {} // 环绕通知,在方法执行后记录日志 @Around("logPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 先执行原方法 Object result = joinPoint.proceed(); // 方法名 String methodName = joinPoint.getSignature().getName(); // 参数 Object[] args = joinPoint.getArgs(); // 写入日志表 AdLog log = new AdLog(); log.setMethod(methodName); log.setParams(Arrays.toString(args)); log.setCreateTime(new Date()); adLogMapper.insert(log); return result; } }逻辑说明:@Aspect和@Component让 Spring 识别这个切面。@Pointcut用 execution 表达式匹配方法名,add*、update*、delete*覆盖了常见的增删改操作。@Around先执行原方法,再记录日志,这样如果原方法抛异常,日志不会写,符合「操作成功才记录」的逻辑。参数上,joinPoint.getArgs()拿到的是方法参数数组,Arrays.toString转成字符串存库,方便排查。
5.2 日志表设计和查询优化
日志表不用太复杂,但索引要加对。我一般会建这样的表:
CREATE TABLE ad_log ( id INT PRIMARY KEY AUTO_INCREMENT, method VARCHAR(64) NOT NULL, params TEXT, create_time DATETIME NOT NULL, INDEX idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:method存方法名,params存参数,create_time存时间。索引加在create_time上,因为查日志通常是按时间范围查。参数上,params用TEXT而不是VARCHAR(255),因为参数可能很长,比如批量删除的 id 列表。
查询的时候用分页,不要一次查全部:
SELECT id, method, params, create_time FROM ad_log WHERE create_time BETWEEN #{start} AND #{end} ORDER BY create_time DESC LIMIT #{offset}, #{limit};逻辑说明:BETWEEN走索引,LIMIT做分页。参数上,offset和limit由前端传页码和每页条数,Service 层算好再传给 Mapper。
5.3 一个我踩过的坑:AOP 切不到 Controller
一开始我把切点写成execution(* com.ad.controller.*.*(..)),想记录所有 Controller 的请求,结果发现日志表里一条记录都没有。原因是 SpringMVC 的 Controller 和 Spring 的 AOP 代理不在同一个容器里,spring-mvc.xml和applicationContext.xml是父子容器关系,AOP 配置在父容器里,切不到子容器的 Controller。
后来我把切点改到 Service 层,问题就解决了。如果你确实想记录 Controller 层的请求,可以在spring-mvc.xml里也开启<aop:aspectj-autoproxy/>,但更推荐的做法是加一个拦截器(Interceptor),在preHandle里记录请求信息。
这个坑我花了两个小时才定位到,希望帮到你。做 SSM 项目,父子容器的边界一定要心里有数,不然很多「配置明明写了却不生效」的问题都会找上门。
本文还有配套的精品资源,点击获取