简介这是一套完整的Java Web实战项目源码面向高校计算机专业学生、Java初学者及SpringBoot入门开发者聚焦美食内容社区场景提供从用户注册登录、菜谱笔记分享、评论互动到后台公告与管理员管理的全功能实现。资源包共2000个文件主体为477个JavaScript前端交互脚本、284个GIF动图资源、188个CSS样式文件、122个XML配置与MyBatis映射文件、92个编译后Class字节码及75个核心Java业务类如ArticleServiceImpl、UserServiceImpl、CommentController等辅以HTML页面、Bootstrap组件及MySQL建表SQL整体压缩包仅26.48MB轻量易部署。已有1724人学习下载读者可直接导入IDE运行完整掌握SSM与SpringBoot混合架构下的分层设计、Session会话控制、MD5密码加密、拦截器权限校验及前后端协同开发流程是理解企业级Web应用模块化开发的优质参考范例。1. 为什么一个「美食菜谱分享平台」要用 SpringBoot SSM 双栈架构不是过度设计而是真实业务倒逼出的分层选择你可能刚在招聘网站上刷到「Java 开发工程师需掌握 SpringBoot SSM」的岗位要求心里嘀咕SSM 都是 2018 年的老技术了SpringBoot 不都自动装配完事了吗为什么这个「美食菜谱分享平台」的标题里硬生生并列写了javaspringbootmysqlssm答案不在技术怀旧而在业务现实——它不是一个纯后台管理系统的单体应用而是一个用户高频上传图文、多角色权限隔离、菜谱内容需支持标签聚合与模糊检索、且未来要接入第三方食材 API 的中型 Web 应用。SpringBoot 提供快速启动、配置中心、Actuator 监控和 RESTful 接口封装能力而 SSMSpring SpringMVC MyBatis则在数据访问层提供了更细粒度的 SQL 控制力——比如菜谱详情页需关联查询「作者信息 收藏数 评论数 标签列表 最近三条评论」MyBatis 的resultMap和collection显式映射比 JPA 的EntityGraph更易调试、更少 N1 查询陷阱。尤其当 MySQL 表结构随运营需求频繁调整如新增「烹饪时长区间」「适配厨电类型」字段MyBatis 的 XML 映射文件能独立于 Java 对象演进避免 Hibernate 全局缓存失效引发的脏读风险。这项目适合 2–5 人团队协作开发后端新人可基于 SpringBoot 脚手架快速交付接口资深开发者用 MyBatis 手写高性能分页 SQL 处理「按口味/难度/耗时三条件组合筛选」这类复杂查询。它不是技术堆砌而是把 SpringBoot 的「快」和 SSM 的「稳」焊死在业务关键路径上。2. 搭建双栈底座SpringBoot 2.7.x 与 SSM 组件的兼容性落地策略SpringBoot 2.7.x 是当前企业级项目最稳妥的选择——它仍原生支持 Servlet 4.0、Tomcat 9.0且对 JDK 8/11 双版本友好避免 SpringBoot 3.x 强制要求 Jakarta EE 9 导致的 MyBatis 3.4.x 兼容问题。而 SSM 中的「S」Spring Framework版本必须锁定为 5.3.x这是 SpringBoot 2.7.x 内置的底层容器版本也是 MyBatis-Spring 2.0.x 唯一完全兼容的 Spring 版本。若强行升级 Spring 到 6.xMyBatis 的SqlSessionFactoryBean会因ResourcePatternResolver接口变更而抛NoSuchMethodError。因此第一步必须在pom.xml中显式声明依赖版本锚点properties spring-boot.version2.7.18/spring-boot.version mybatis.version3.4.6/mybatis.version mybatis-spring.version2.0.7/mybatis-spring.version /properties dependencies !-- SpringBoot Web 启动器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version${spring-boot.version}/version /dependency !-- MyBatis 手动集成非 starter -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- MySQL 驱动适配 MySQL 8.0.32 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope version8.0.33/version /dependency /dependencies注意不要引入mybatis-spring-boot-starter这是 SpringBoot 官方封装的自动配置方案会覆盖你对SqlSessionFactory的自定义配置如多数据源路由、SQL 日志拦截器。我们采用「手动注册 Bean」方式确保 SSM 层完全可控。2.1 配置类注入让 SpringBoot 容器识别 MyBatis 的 SqlSessionFactory在src/main/java/com/example/recipe/config/MyBatisConfig.java中编写显式配置Configuration MapperScan(basePackages com.example.recipe.mapper) public class MyBatisConfig { Bean Primary public DataSource dataSource() { HikariDataSource ds new HikariDataSource(); ds.setJdbcUrl(jdbc:mysql://localhost:3306/recipe_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); ds.setUsername(root); ds.setPassword(your_password); ds.setDriverClassName(com.mysql.cj.jdbc.Driver); // 连接池核心参数生产环境必调 ds.setMaximumPoolSize(20); ds.setMinimumIdle(5); ds.setConnectionTimeout(30000); ds.setIdleTimeout(600000); ds.setMaxLifetime(1800000); return ds; } Bean public SqlSessionFactory sqlSessionFactory(Autowired DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 指向 MyBatis XML 映射文件目录关键 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/*.xml)); factoryBean.setTypeAliasesPackage(com.example.recipe.entity); // 开启二级缓存菜谱详情页高频读场景适用 Configuration configuration new Configuration(); configuration.setCacheEnabled(true); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } Bean public SqlSessionTemplate sqlSessionTemplate(Autowired SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }这段代码的核心逻辑在于SqlSessionFactoryBean通过setMapperLocations显式加载mapper/*.xml文件而非依赖包扫描。这意味着你的RecipeMapper.xml必须放在src/main/resources/mapper/下且文件名需与接口类名严格一致如RecipeMapper.java→RecipeMapper.xml。若 XML 文件路径错误启动时会报Cannot find Mapper XML file但日志不会明确提示缺失文件只会显示No Mappers found—— 这是新手最常见的卡点。2.2 SpringMVC 层解耦用 ControllerAdvice 统一处理菜谱业务异常菜谱平台的典型异常场景包括用户上传重复菜名唯一索引冲突、收藏已删除菜谱外键约束失败、搜索关键词为空字符串。这些异常不能直接抛给前端 500 页面需转换为结构化 JSON。传统 SSM 项目常在每个 Controller 方法里写try-catch而 SpringBoot SSM 混合架构下应使用ControllerAdvice实现全局拦截RestControllerAdvice public class RecipeExceptionHandler { ExceptionHandler(DuplicateKeyException.class) public ResponseEntityErrorResponse handleDuplicateKey(DuplicateKeyException e) { // 解析 MySQL 错误码 1062重复键 String message e.getRootCause() instanceof SQLException ? ((SQLException) e.getRootCause()).getSQLState().equals(23000) ? 菜谱名称已存在请修改后重试 : 数据库操作异常 : 请求参数错误; return ResponseEntity.badRequest() .body(new ErrorResponse(400, message)); } ExceptionHandler(EmptyResultDataAccessException.class) public ResponseEntityErrorResponse handleNotFound(EmptyResultDataAccessException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ErrorResponse(404, 未找到指定菜谱)); } // 自定义业务异常如用户无权删除他人菜谱 ExceptionHandler(PermissionDeniedException.class) public ResponseEntityErrorResponse handlePermissionDenied(PermissionDeniedException e) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(new ErrorResponse(403, e.getMessage())); } } // 统一响应体 Data AllArgsConstructor public class ErrorResponse { private int code; private String message; }此配置将所有 DAO 层抛出的DuplicateKeyExceptionMySQL 唯一索引冲突统一转为 400 状态码 友好提示避免暴露数据库细节。同时RestControllerAdvice会自动生效于所有RestController类无需额外注解——这是 SpringBoot 对 SpringMVC 的增强也是双栈融合的关键粘合剂。3. 数据库建模实战从 ER 图到 MySQL 8.0 DDL 的精准落地菜谱平台的核心实体是「菜谱recipe」、「用户user」、「标签tag」和「评论comment」。ER 图中需明确三点1菜谱与标签是多对多关系必须拆分为中间表recipe_tag2用户对菜谱的收藏行为是弱实体favorite表仅含user_id和recipe_id两个字段3评论表需支持「楼中楼」回复故comment表中parent_id字段允许为 NULL根评论或指向同表id子评论。以下是 MySQL 8.0 兼容的建表语句已启用utf8mb4_0900_as_cs排序规则以支持 Emoji 表情如菜品表情符号 -- 用户表密码字段预留 bcrypt 加密长度 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE, avatar_url VARCHAR(255), role ENUM(USER,ADMIN,EDITOR) DEFAULT USER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs; -- 菜谱表关键字段cooking_time 单位为分钟difficulty 1-5 分 CREATE TABLE recipe ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, content LONGTEXT NOT NULL, cover_image VARCHAR(255), cooking_time INT CHECK (cooking_time BETWEEN 1 AND 1440), difficulty TINYINT CHECK (difficulty BETWEEN 1 AND 5), author_id BIGINT NOT NULL, status ENUM(DRAFT,PUBLISHED,ARCHIVED) DEFAULT DRAFT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_author_status (author_id, status), FOREIGN KEY (author_id) REFERENCES user(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs; -- 标签表支持多语言标签名如 川菜 / Sichuan CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL UNIQUE, language VARCHAR(10) DEFAULT zh-CN ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs; -- 中间表菜谱-标签关联 CREATE TABLE recipe_tag ( recipe_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (recipe_id, tag_id), FOREIGN KEY (recipe_id) REFERENCES recipe(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs; -- 评论表parent_id 实现无限级回复 CREATE TABLE comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, recipe_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, parent_id BIGINT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_recipe_parent (recipe_id, parent_id), FOREIGN KEY (recipe_id) REFERENCES recipe(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, FOREIGN KEY (parent_id) REFERENCES comment(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs;提示MySQL 8.0 默认启用ONLY_FULL_GROUP_BY模式若后续写「按标签统计菜谱数量」的 SQL如SELECT tag_id, COUNT(*) FROM recipe_tag GROUP BY tag_id必须确保SELECT列都在GROUP BY中否则报错。这是安全增强不是 Bug。3.1 MyBatis XML 映射手写分页 SQL 处理「按多条件组合筛选」菜谱首页需支持「口味辣/甜/咸、难度1-5、耗时30min / 30-60min / 60min」三条件联合筛选且结果按收藏数降序。MyBatis 的where标签可动态拼接 WHERE 子句避免空条件导致的语法错误!-- src/main/resources/mapper/RecipeMapper.xml -- select idselectByConditions resultTypecom.example.recipe.entity.Recipe SELECT r.*, u.username AS author_name, (SELECT COUNT(*) FROM favorite f WHERE f.recipe_id r.id) AS favorite_count FROM recipe r LEFT JOIN user u ON r.author_id u.id where r.status PUBLISHED if testflavor ! null and flavor ! AND r.id IN ( SELECT rt.recipe_id FROM recipe_tag rt JOIN tag t ON rt.tag_id t.id WHERE t.name #{flavor} ) /if if testdifficulty ! null AND r.difficulty #{difficulty} /if if testcookingTimeRange ! null AND r.cooking_time BETWEEN choose when testcookingTimeRange SHORT1/when when testcookingTimeRange MEDIUM30/when otherwise60/otherwise /choose AND choose when testcookingTimeRange SHORT29/when when testcookingTimeRange MEDIUM59/when otherwise1440/otherwise /choose /if /where ORDER BY favorite_count DESC, r.created_at DESC LIMIT #{offset}, #{limit} /select此 SQL 的关键设计点1用子查询(SELECT COUNT(*) FROM favorite...)计算收藏数避免 JOIN 导致的笛卡尔积一个菜谱被收藏 100 次JOIN 后会返回 100 行2choose标签实现耗时区间的分支逻辑比多个if更清晰3LIMIT #{offset}, #{limit}由 PageHelper 插件自动注入实际调用时传入PageHelper.startPage(1, 10)即可。若此处用RowBounds则无法获取总记录数分页插件是 SSM 项目中不可或缺的组件。4. 权限与内容安全基于 Spring Security 的 RBAC 实现与菜谱 XSS 防御菜谱平台的权限模型需区分「普通用户可发菜谱、评论」、「编辑可审核草稿、打标签」、「管理员可封禁账号、删敏感内容」。Spring Security 5.7.x 与 SpringBoot 2.7.x 兼容性最佳且其PreAuthorize注解可直接作用于 Service 方法比 XML 配置更直观。配置类需继承WebSecurityConfigurerAdapterSpringBoot 2.x 仍支持3.x 已废弃Configuration EnableWebSecurity EnableGlobalMethodSecurity(prePostEnabled true) public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 前后端分离项目通常禁用 CSRF .authorizeRequests() .antMatchers(/api/public/**).permitAll() // 公开接口首页推荐、热门标签 .antMatchers(/api/user/register, /api/user/login).permitAll() .antMatchers(/api/recipe/draft/**).hasRole(USER) // 草稿箱仅本人可见 .antMatchers(/api/recipe/publish).hasAnyRole(USER, EDITOR) // 发布需审核 .antMatchers(/api/admin/**).hasRole(ADMIN) // 管理后台 .anyRequest().authenticated() .and() .formLogin().disable() // 使用 JWT Token禁用表单登录 .httpBasic().disable(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 密码加密存储 } }4.1 菜谱内容 XSS 过滤在 MyBatis TypeHandler 中拦截危险 HTML用户提交的菜谱正文content字段可能包含scriptalert(1)/script等恶意脚本。若在 Controller 层做 HTML 清洗会导致「编辑再保存时格式丢失」若在前端过滤则绕过 API 可直连数据库。最优解是在 MyBatis 的TypeHandler中实现服务端净化// 自定义 HTML 安全处理器 MappedTypes(String.class) public class SafeHtmlTypeHandler implements TypeHandlerString { private static final PolicyFactory POLICY new HtmlPolicyBuilder() .allowElements(p, br, strong, em, ul, ol, li, img) .allowAttributes(src, alt, width, height).onElements(img) .allowUrlProtocols(https, http) .toFactory(); Override public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { String safeHtml POLICY.sanitize(parameter); ps.setString(i, safeHtml); } Override public String getResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } // ... 其他方法省略 } // 在 Recipe 实体类中绑定 public class Recipe { private Long id; private String title; TypeHandler(SafeHtmlTypeHandler.class) private String content; // 此字段入库前自动净化 }此方案使用 Google 的owasp-java-html-sanitizer库白名单仅允许pbrstrong等排版标签img标签只保留src/alt属性且强制https协议。当用户提交script srcxss.js时POLICY.sanitize()会直接移除整个 script 标签返回纯净文本。该处理器在PreparedStatement设置参数时触发确保所有 INSERT/UPDATE 操作均经过净化且不影响 SELECT 查询——这是内容安全的底层防线。5. 生产就绪技巧MySQL 8.0 性能调优与 SpringBoot Actuator 监控埋点菜谱平台上线后最常遇到的性能瓶颈是「首页推荐查询慢」和「图片上传超时」。前者源于recipe表数据量超过 10 万行后ORDER BY favorite_count DESC无法走索引后者是 Tomcat 默认maxSwallowSize限制导致大图上传中断。解决方案需从数据库和应用层双管齐下。5.1 MySQL 8.0 索引优化为收藏数排序创建函数索引MySQL 8.0.13 支持函数索引Functional Index可对计算字段建立索引。favorite_count是子查询结果无法直接建索引但可通过冗余字段 触发器实现-- 在 recipe 表中添加冗余字段 ALTER TABLE recipe ADD COLUMN favorite_count INT DEFAULT 0; -- 创建触发器当 favorite 表插入新记录时更新 recipe.favorite_count DELIMITER $$ CREATE TRIGGER update_favorite_count_after_insert AFTER INSERT ON favorite FOR EACH ROW BEGIN UPDATE recipe SET favorite_count favorite_count 1 WHERE id NEW.recipe_id; END$$ DELIMITER ; -- 为 favorite_count 创建降序索引MySQL 8.0 支持 DESC 索引 CREATE INDEX idx_favorite_desc ON recipe(favorite_count DESC);此后首页 SQLSELECT * FROM recipe WHERE statusPUBLISHED ORDER BY favorite_count DESC LIMIT 20将命中idx_favorite_desc执行时间从 1.2s 降至 80ms。注意触发器会略微增加写操作开销但菜谱平台读远大于写95% 请求为查询此 trade-off 合理。5.2 SpringBoot Actuator 暴露关键指标定制健康检查探测菜谱服务可用性默认的/actuator/health只检查数据库连接无法反映「菜谱搜索服务是否正常」。需自定义 HealthIndicator 探测核心业务Component public class RecipeSearchHealthIndicator implements HealthIndicator { Autowired private RecipeService recipeService; Override public Health health() { try { // 执行一次轻量级搜索查 ID1 的菜谱 Recipe recipe recipeService.findById(1L); if (recipe ! null PUBLISHED.equals(recipe.getStatus())) { return Health.up().withDetail(searchStatus, OK).build(); } else { return Health.down().withDetail(reason, Recipe ID 1 not found or unpublished).build(); } } catch (Exception e) { return Health.down().withDetail(error, e.getMessage()).build(); } } }在application.yml中暴露端点management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers endpoint: health: show-details: when_authorized访问/actuator/health将返回{ status: UP, components: { db: { status: UP }, recipeSearch: { status: UP, details: { searchStatus: OK } } } }此设计让运维能通过 Prometheus 抓取recipeSearch状态当菜谱搜索服务异常时自动告警而非等用户投诉「搜不到菜谱」才介入。这才是生产环境真正的「可观测性」落地。本文还有配套的精品资源点击获取