ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot厨艺交流平台实战:从自动装配到缓存与定时任务

Spring Boot厨艺交流平台实战:从自动装配到缓存与定时任务 看见“Springboot的厨艺交流平台--005”这个标题第一反应就是这不又是一个典型的“黑马式”项目嘛。说得直白点单独拎出“厨艺交流”四个字谁都能脑补出一堆菜谱列表、美食帖子、评论收藏但真正的关键从来不在业务场景里而在“Springboot”这几个字上。它决定了你这个平台是跑几天就崩的教学demo还是能扛住真实访问量的基础架构。我拿这个编号为005的项目完整做了一遍从IDEA初始化、数据库建模、核心接口开发到前后端联调一路踩了不少坑。这篇就把整个思路、关键实现和排错经验整体写出来代码都在线上照着敲完你得到的不仅是一个“能做菜谱CRUD”的练习品而是一个能讲清楚Spring Boot自动装配、缓存策略、定时任务和接口规范的完整案例。1. 项目拆解厨艺交流平台到底在做什么1.1 需求定位它的核心是一个UGC内容平台先说业务。厨艺交流平台本质上是一个UGC用户生成内容社区不是企业级管理系统那种纯CRUD。管理系统的数据流向是单向的、封闭的而内容社区的数据是双向的、开放的。用户注册登录后不光自己发菜谱还要看别人的、点赞、收藏、评论甚至关注某个做饭好吃的作者。这种模式对后端工程的要求比“图书管理”这种课设高一个档次。如果一定要给这个平台划核心模块我把它分成四块用户中心注册、登录、个人主页、密码加密、Token鉴权内容中心菜谱的发布、编辑、上下架、图文排版、食材步骤拆解互动中心评论留言、点赞、收藏、浏览量统计检索与推荐按分类筛选、关键词搜索、热门榜。这四块合在一起才叫“交流平台”。你要是只做了第一块和第二块那只能叫“菜谱发布系统”和“交流”两个字完全不沾边。我见过很多毕设项目挂个“交流平台”的名字结果评论和点赞全是摆设这是最大的误区。1.2 技术选型为什么用Spring Boot而不是SSM现在很多人上来就问“为什么用Spring Boot”。我的回答很简单因为Spring Boot解决的是SSM时代最折磨人的配置问题让你把精力花在业务本身。打个比方SSM就像你租了个毛坯房住进去之前要自己刷墙、铺地、接水电甚至改户型Spring Boot则是精装房拎包入住每个房间该有的东西都给你预装好了你只需要按自己的需求摆家具就行。在厨艺交流平台这种以“交付业务功能”为主的项目里精装房显然更合理。具体来说Spring Boot带给我这个项目的几个核心收益Starter机制引入一个依赖就等于把一组相关的依赖和自动配置全部搞定比如spring-boot-starter-web直接就把Spring MVC、内嵌Tomcat、Jackson序列化全部带进来了自动装配数据库连接池、MyBatis、Redis这些组件的初始化不再需要手写一坨XML配置内嵌容器项目打成jar包直接java -jar启动部署成本比外置Tomcat低太多生态成熟无论是做Redis缓存、JWT鉴权还是定时任务Spring Boot都有对应的start jar社区资料极其丰富。当然Spring Boot不是万能药。它默认的封装在带来便利的同时也让很多人“只会用不会调”。一旦项目里出现自动装配排序问题、高版本JDK兼容问题不懂原理就完全抓瞎。所以后面我会专门花一节讲自动装配的底层逻辑这个是面试必问也是排查问题的基础。1.3 功能模块设计的几个关键决策先展示我规划的功能总览后面逐个实现模块核心功能技术支撑用户模块注册登录、Token鉴权、个人信息维护Spring Security JWT菜谱模块菜谱发布、编辑、上下架、图片上传Spring MVC 文件上传分类模块菜系分类、标签管理MyBatis-Plus互动模块评论、点赞、收藏、浏览量统计Redis 定时任务落库检索模块标题/食材模糊搜索、热门菜谱排行MySQL Redis ZSet接口模块API文档、统一返回结构、全局异常处理springdoc-openapi这里有三个关键的决策点需要解释一下。第一为什么点赞和收藏要用Redis而不是直接写MySQL因为“点赞”和“收藏”是高频率、低价值的写操作用户手一抖可能就点好几下每次都去Update数据库不仅慢还会给数据库造成很大压力。Redis擅长处理这种高并发计数场景先写到内存里再通过定时任务批量落库性能和稳定性都更好。第二为什么JWT而不是Session前后端分离架构下前端是Vue后端是Spring Boot两边不在同一个域Session复制和Cookie跨域都是大麻烦。JWT把用户信息加密后放在Token里前端每次请求带在Header里后端无状态校验天然适合横向扩展。第三为什么选MyBatis-Plus而不是纯MyBatis说实话这个项目里80%的数据库操作都是单表CRUDMyBatis-Plus的BaseMapper直接把20行代码压缩成1行。剩下的20%多表查询、统计SQL手写XML也能很优雅地搞定。选它既省时间又不会让人觉得你只会“面向百度编程”。2. Spring Boot项目初始化与核心机制2.1 IDEA创建Spring Boot项目的正确姿势与版本选择先说说项目初始化。热搜词里反复出现“idea创建springboot项目”和“springboot版本太高”说明很多人第一步就卡住了。我用IDEA创建一个标准Spring Boot项目的大致流程是这样打开IDEA选New Project-Spring InitializrServer URL选start.spring.io网络不稳定就用阿里云镜像https://start.aliyun.comGroup填com.exampleArtifact填cooking-platformJava版本这里要特别注意很多人就是在这里栽跟头选择依赖Web、MyBatis-Plus阿里云镜像有、MySQL Driver、Redis、Lombok、Validation点击Finish等待Maven下载依赖。版本选择是我必须单独拎出来讲的一环。很多新手直接默认选最新稳定版Spring Boot 3.x结果发现JDK要17项目里用的老代码还在用javax包全报红然后就开始痛苦回退。这里我直接给一套没有坑的搭配方案场景推荐版本说明毕设/课设/新手练习Spring Boot 2.7.18 JDK 8资料最多踩坑最容易查企业新项目无历史包袱Spring Boot 3.2.x JDK 17性能更好但生态迁移有一点成本老项目升级按当前JDK逐步升级别跨大版本2.x到3.x是重构不是升级我们这个厨艺交流平台我用的就是Spring Boot 2.7.18 JDK 8的组合。不是说它最新而是它最稳。2.7是Spring Boot 2.x的最终版本该修的bug都修了同时还能兼容大量老项目代码网上随便搜个问题都能找到答案。如果IDEA默认的JDK是21而你想用JDK 8不需要卸载JDK 21。在IDEA的Project Structure里加一个JDK 8的SDK然后在Project和Modules里分别指定即可。另外一个容易忽略的点pom.xml里要加java.version8/java.version同时maven-compiler-plugin的source和target也设为8。2.2 自动装配原理为什么引入依赖就能直接用你要是只把Spring Boot当“快速脚手架”用那这篇文章看到这里就可以关了。但如果你后续要面试或者要排查问题自动装配原理这事必须搞清楚因为热搜词里“springboot自动装配原理”出现了不止一次这是Spring Boot最核心的设计。我把自动装配拆成三个问题来讲。第一自动装配的入口在哪答案是主启动类上的SpringBootApplication注解。这个注解是一个组合注解里面包含了SpringBootConfiguration、EnableAutoConfiguration和ComponentScan。其中真正干“自动装配”活儿的是EnableAutoConfiguration。第二Spring Boot怎么知道要装配哪些组件关键在于AutoConfiguration.imports文件。在Spring Boot 2.7版本里路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里面列了一长串自动配置类。比如RedisAutoConfiguration、DataSourceAutoConfiguration、WebMvcAutoConfiguration都在这个文件里注册。第三这些自动配置类怎么做到“按需加载”答案是条件注解。每个自动配置类上都会有一堆条件注解比如ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty。拿RedisAutoConfiguration举例它在classpath里发现RedisOperations这个类说明你引入了Redis的starter就会创建一个RedisTemplate和StringRedisTemplate注入容器如果你压根没引入Redis依赖它就直接跳过当什么事都没发生。所以自动装配的完整链路是引入依赖 - 条件注解判断生效 - 读取AutoConfiguration.imports- 装载对应的配置类 - 通过Bean创建组件 - 组件配合ConfigurationProperties绑定配置文件里的前缀属性。这个机制给厨艺交流平台的直接影响就是我想让项目支持Redis只需要在pom.xml里加一个spring-boot-starter-data-redis然后在application.yml里配好spring.redis.host等地址RedisTemplate就能直接用了。放在以前SSM时代你得手动配置Jedis连接池、自己写工具类还要配置什么连接工厂一套下来小半天就没了。2.3 手写厨艺平台的核心配置文件我把这个项目的application.yml关键配置贴出来每个配置的解释都会写上方便你照着改。server: port: 8081 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cooking_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 redis: host: localhost port: 6379 database: 0 lettuce: pool: max-active: 16 max-idle: 8 servlet: multipart: max-file-size: 5MB max-request-size: 20MB核心配置里有两个点很容易被忽略。第一个是hikari连接池参数HikariCP是Spring Boot默认的数据源连接池号称史上最快。maximum-pool-size不是越大越好默认10其实足够支撑一个小型内容平台的日常流量因为一次请求用的连接时间都很短连接池主要应对的是“短时间大量并发”的场景。第二个是multipart上传大小限制不配这个的话用户发菜谱传一张超过1MB的图片就会被拒绝默认只有1MB对美食图片来说完全不够用至少要调到5MB。3. 数据库设计与核心接口实现3.1 厨艺平台到底需要几张表很多做这种项目的人上来就建表边写边加字段最后表结构乱得一塌糊涂。我的习惯是先花半小时把表设计好后面写代码的速度反而最快。厨艺交流平台的表结构我做了这样的设计-- 用户表 CREATE TABLE tb_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, phone varchar(20) DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci; -- 菜谱分类表 CREATE TABLE tb_category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(30) NOT NULL COMMENT 分类名川菜/粤菜/烘焙/家常菜, description varchar(255) DEFAULT NULL, sort int DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜谱主表 CREATE TABLE tb_recipe ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 发布者ID, category_id bigint NOT NULL COMMENT 分类ID, title varchar(100) NOT NULL COMMENT 菜名, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, intro varchar(500) DEFAULT NULL COMMENT 简介, difficulty tinyint DEFAULT 2 COMMENT 难度1简单 2中等 3困难, cook_time int DEFAULT NULL COMMENT 烹饪时长(分钟), status tinyint DEFAULT 0 COMMENT 0草稿 1已发布 2下架, view_count bigint DEFAULT 0 COMMENT 浏览量, like_count bigint DEFAULT 0 COMMENT 点赞数, favorite_count bigint DEFAULT 0 COMMENT 收藏数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_category (category_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜谱食材表 CREATE TABLE tb_recipe_ingredient ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, name varchar(50) NOT NULL COMMENT 食材名, amount varchar(50) DEFAULT NULL COMMENT 用量如300克, PRIMARY KEY (id), KEY idx_recipe (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜谱步骤表 CREATE TABLE tb_recipe_step ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, step_num int NOT NULL COMMENT 第几步, description text NOT NULL, image varchar(255) DEFAULT NULL COMMENT 步骤图, PRIMARY KEY (id), KEY idx_recipe (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评论表 CREATE TABLE tb_comment ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, user_id bigint NOT NULL, parent_id bigint DEFAULT 0 COMMENT 0表示一级评论, content varchar(1000) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_recipe (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 收藏表 CREATE TABLE tb_favorite ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, recipe_id bigint NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_recipe (user_id, recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个设计在业务上复刻了主流菜谱App的底层数据模型。几个细节说明一下tb_recipe里同时冗余了like_count和favorite_count字段很多人觉得冗余但这其实是有意的。点赞数、收藏数属于高频读取、低频精确查询的聚合数据页面列表页显示菜谱卡片时根本不需要实时去count评论表和收藏表直接读冗余字段性能快得多。数据最终通过Redis异步统计后再回写。步骤表和食材表都只有外键索引而没有外键约束这是阿里Java开发规范里推荐的。外键约束在数据量上来后对性能损耗明显而且跨表更新极其痛苦实际操作中通过应用层逻辑保证数据一致性就够了。全文按utf8mb4存储这点非常重要。MySQL默认的utf8实际上是utf8mb3根本存不了emoji表情。厨艺交流平台上用户评论里发个“太好吃了吧”就直接报错换成utf8mb4就能存。3.2 用户认证与JWT鉴权实现用户模块是所有其他模块的地基。我先实现一个标准的JWT工具类负责生成Token和解析TokenComponent public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 过期时间(毫秒) public String generateToken(Long userId, String username) { Date now new Date(); Date expireTime new Date(now.getTime() expire); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(now) .setExpiration(expireTime) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Long getUserId(String token) { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return Long.parseLong(claims.getSubject()); } }JWT有个非技术但是特别重要的坑secret必须足够长。很多人随手写个abc123秘钥长度不够HS256签名算法会直接抛WeakKeyException。一般要求是至少32个字节。我的习惯是用UUID生成一串再手动改掉中间的横杠长度64位绝对安全。登录接口的逻辑不复杂先根据用户名查用户再用BCryptPasswordEncoder去匹配密码。匹配成功就生成Token返回前端同时把用户基本信息回传。这里强调一下密码存储必须用BCrypt不能用MD5或者SHA这类不可逆加盐算法。BCrypt的妙处在于每次加密同一个密码得到的结果都不一样因为它在内部自动引入了随机盐即使两个用户的密码相同数据库里存的哈希值也完全不同。鉴权这块我用了Spring拦截器的方式实现一个LoginInterceptorComponent public class LoginInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Long userId jwtUtil.getUserId(token); // 把userId放到request attribute里后续controller直接取 request.setAttribute(userId, userId); return true; } catch (Exception e) { // 解析失败token过期或无效 } } response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录或Token已过期\}); return false; } }然后注册拦截器并配置放行路径。厨艺平台的注册、登录、菜谱首页列表、菜谱详情这些公开接口可以匿名访问但发表评论、收藏菜谱、发布菜谱等操作必须登录Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /auth/login, /auth/register, /recipe/public/**, /category/**, /file/**, /doc.html, /webjars/**, /v3/api-docs/** ); } }热搜词里提到的“springboot 常用注解”在这里基本都用上了。RestController、RequestMapping、PathVariable、RequestBody、RequestParam再加一个Validated做参数校验。多练几个接口你对这几个注解就会形成肌肉记忆。3.3 菜谱发布与图片上传实战菜谱发布是整个平台最核心的写操作。前端提交的数据结构是一个菜谱主体 食材列表 步骤列表这就引出了一个经典问题前端应该一次提交还是分多次提交一次提交。我用一个RecipeDTO来接收整个请求体Data public class RecipeCreateDTO { NotNull(message 分类不能为空) private Long categoryId; NotBlank(message 菜名不能为空) Size(max 100) private String title; private String coverImage; Size(max 500) private String intro; private Integer difficulty; private Integer cookTime; private ListIngredientDTO ingredients; Valid private ListStepDTO steps; }然后Service层在一个事务里同时写入主表、食材表和步骤表使用Transactional保证原子性。这个设计很关键比如用户提交了一道菜主表写进去了但步骤表写入时数据库报错如果没有事务就会出现“菜谱详情打不开因为步骤是空的”这种脏数据。图片上传的实现也不难我做了统一的上传接口RestController RequestMapping(/file) public class FileController { Value(${file.upload-path}) private String uploadPath; PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); // 生成不重复的文件名 String filename UUID.randomUUID().toString().replace(-, ) suffix; File dest new File(uploadPath filename); try { file.transferTo(dest); } catch (IOException e) { return Result.error(上传失败 e.getMessage()); } return Result.success(/upload/ filename); } }这里用的是本地磁盘存储适合开发和毕设场景。生产环境一般会用阿里云OSS、MinIO或者腾讯云COS核心思路都一样前端把文件传给你你把它存到云端返回一个可访问的URL。区别只是Sdk调用方式不同。图片上传有个安全细节必须注意不要信任前端传过来的文件名。文件名可能是../../etc/passwd这种恶意路径也可能是中文名、带空格的。直接用原始文件名拼接路径会引发路径穿越漏洞。正确的做法是忽略原始文件名服务端用UUID生成全新的随机文件名再把后缀白名单校验一下只允许jpg、jpeg、png、gif、webp几类。3.4 Redis缓存菜谱详情与热榜排名菜谱详情是厨艺交流平台中读压力最大的接口。用户打开首页看到20个菜谱卡片每个卡片都要显示浏览量、点赞数、收藏数点进详情页又要查一次完整的菜谱信息。如果所有的读请求都压到MySQL上并发稍微一上去数据库CPU就会飙升连接池被打满然后整个应用卡死。我在这个项目里给菜谱详情加了Redis缓存逻辑是这样的优先查Redis缓存key是recipe:detail:{id}查不到就查数据库查完回填Redis并设置过期时间缓存过期时间设置成随机值防止大量key同一时间过期导致缓存雪崩。代码实现public RecipeVO getRecipeDetail(Long id) { String key recipe:detail: id; String cached stringRedisTemplate.opsForValue().get(key); if (cached ! null) { return JSON.parseObject(cached, RecipeVO.class); } RecipeVO recipe getRecipeFromDb(id); // 实际项目里这里要做多表联查 if (recipe ! null) { int expireTime 300 new Random().nextInt(300); stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(recipe), expireTime, TimeUnit.SECONDS); } return recipe; }菜谱详情缓存最大的坑在于数据一致性。用户修改菜谱内容或者把菜谱下架后Redis里可能还留着旧数据。我的处理方式是双删策略先在数据库里执行更新操作然后删除Redis缓存。为什么是删除而不是更新因为更新缓存的代价更高、并发风险更大而删除掉之后下次请求再回填即可这种惰性加载的方式足够满足这个场景。热榜排行的实现用了Redis的ZSet结构。用户每浏览一次菜品我就对recipe:hot这个ZSet执行一次incrementScore以菜谱id为member、点击量或综合热度为scorepublic void recordView(Long recipeId) { String key recipe:hot; stringRedisTemplate.opsForZSet().incrementScore(key, String.valueOf(recipeId), 1); } public ListLong getHotRecipeIds(int size) { String key recipe:hot; SetString ids stringRedisTemplate.opsForZSet() .reverseRange(key, 0, size - 1); return ids.stream().map(Long::parseLong).collect(Collectors.toList()); }这个方案在生产环境也玩得转。ZSet的reverseRange拿到分数最高的N个成员直接作为首页“热门菜谱”栏目的数据源。之所以不用MySQL的ORDER BY view_count DESC LIMIT 10是因为这个查询在数据量起来后非常重每次都要全表排序而Redis的ZSet是高性能的跳表结构这种Top N排行就是它的看家本领。3.5 定时任务与搜索引擎扩展思路Spring Boot的定时任务很简单在启动类上加EnableScheduling然后在需要定时执行的类方法上加Scheduled注解即可。我在厨艺交流平台里用两个定时任务每天凌晨2点把Redis里的浏览量同步到MySQL每天凌晨3点计算前一天的各菜系热门菜谱写入一张推荐表供首页“今日推荐”模块读取。下面是浏览量落库的代码Component Slf4j public class DataSyncTask { Autowired private StringRedisTemplate stringRedisTemplate; Autowired private RecipeMapper recipeMapper; Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void syncViewCount() { // 从Redis中获取所有浏览量key SetString keys stringRedisTemplate.keys(recipe:view:*); if (keys null || keys.isEmpty()) { return; } for (String key : keys) { String recipeId key.substring(recipe:view:.length()); Object countObj stringRedisTemplate.opsForValue().get(key); if (countObj null) continue; long count Long.parseLong(countObj.toString()); // 更新数据库 recipeMapper.updateViewCount(Long.parseLong(recipeId), count); // 删除Redis中的key stringRedisTemplate.delete(key); } log.info(浏览量同步完成共处理 {} 个菜谱, keys.size()); } }Scheduled的cron表达式这里多说一句Spring的cron是6位秒 分 时 日 月 周和Linux的5位cron不一样。很多人第一次写就把“每天的2点”写成0 0 2 * * ?里的最后一位结果任务一直不执行。Spring里第6位如果不需要用?而不是*这两个的区别在周这个字段上很重要。至于热搜词里提到的Hanlp分词、Elasticsearch这些它们在这个项目里的定位是把搜索从“能用”变成“好用”。现在的关键词搜索用的是MySQL的LIKE %keyword%这个方案在数据量几千条的时候没毛病但有一个先天缺陷用户搜“红烧”匹配不到“红烧肉”搜“番茄”匹配不到“西红柿”它只会做纯字面匹配。如果当初我把搜索升级一下方向就是引入Hanlp做分词把菜谱的名称、食材、简介拆成词项存到Elasticsearch里建立倒排索引查询时先用Hanlp把用户输入切词再通过ES的匹配查询返回结果还能顺便做同义词扩展。这个扩展方案做进毕设或者项目亮点展示是能在答辩时加分的。4. 前后端分离场景下的坑与排查实录4.1 跨域问题为什么前端请求总是到不了后端这个项目如果做成前后端分离前端Vue跑在3000端口后端跑在8081端口跨域问题基本躲不掉。浏览器同源策略规定前端http://localhost:3000访问后端http://localhost:8081协议、域名、端口三者只要有一个不一致浏览器就会拦截响应。我在开发阶段用的最简单方案是给控制层加跨域允许但更推荐的是写一个全局的CorsFilterConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); // 实际项目不要用 *这里开发阶段偷懒了 config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个极其常见的坑addAllowedOrigin(*)和setAllowCredentials(true)不能同时使用浏览器会直接拒绝响应。两者同时配的时候通配符*会被浏览器视为不安全必须改用addAllowedOriginPattern(*)。4.2 数据库连接超时与多数据源配置热搜词里“doris springboot 连接数据库设置超时”和“springboot多数据源”这两条其实指向的是同一个问题当你的项目需要同时连接多个数据源或者数据源在高负载下出现连接超时怎么处理。先看连接超时。HikariCP的connection-timeout默认是30秒意思是当连接池里没有空闲连接时线程最多等30秒超时就会报SQLTransientConnectionException。如果你在压测时遇到大量这样的报错不要第一时间加maximum-pool-size而是要去查是不是有长事务把连接占住了。多数据源这个需求在我这个项目里不是必须的但如果你想把业务库和统计库分开Spring Boot的配置思路是这样的spring: datasource: primary: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cooking_platform username: root password: root secondary: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cooking_platform_stat username: root password: root然后在代码里配置两个DataSource标注Primary的那个用于主库另一个用于从库。如果用了MyBatis-Plus还可以引入dynamic-datasource-spring-boot-starter这个包通过DS(secondary)注解在Mapper层直接切换数据源非常方便。4.3 Spring Boot版本与内置Tomcat替换很多人问“springboot版本太高怎么办”或者“idea里JDK是21怎么回退到1.8”这些都是项目初始化阶段的版本管理问题。前面第2节我已经给了推荐方案这里补充一个容易忽略的机制Spring Boot版本和内置Tomcat版本是对应死的。Spring Boot 2.7.x内置Tomcat是9.0系列支持Servlet 4.0和JDK 8Spring Boot 3.x内置Tomcat是10.1系列底层用jakarta.*包替换了javax.*包。如果项目里有用到javax.servlet.http.HttpServletRequest这类代码直接升到Spring Boot 3.x是编译不过的。如果公司要求用国产中间件替换内嵌Tomcat比如要让war包跑在国产中间件上核心步骤有两个第一在pom.xml里把spring-boot-starter-web自带的Tomcat依赖排除掉换成对应的中间件适配依赖第二部署方式改成外置容器war包模式也就是启动类继承SpringBootServletInitializer并重写configure方法。至于怎么做最小化改造我的建议是如果项目没有历史包袱直接用war包标准部署如果有那就先把默认Tomcat跑通再考虑替换不要一开始就两头改造。4.4 Swagger文档与调试常用问题接口文档这块因为Spring Boot 2.7的生态兼容性最好所以我用的是springdoc-openapi这个库。它比老牌的springfox对Spring Boot 2.6的兼容性好得多不需要额外的路径匹配策略配置。依赖引入后访问http://localhost:8081/api/v3/api-docs可以看到JSON格式的API文档访问http://localhost:8081/api/doc.html可以打开Swagger UI调试页面。Swagger使用中有个常见坑接口方法里用了RequestBody接收对象但前端实际提交的是表单格式这时候文档里能测通真实场景却一直报415。原因就是RequestBody只认JSON格式表单提交必须用RequestParam或者直接用实体类接收Spring MVC会自动绑定。这类问题在做前后端联调时特别常见排查思路就是先看Content-Type请求头是不是application/json不是就说明前端传参方式错了。另外一个和接口调试相关的高频问题明明代码里逻辑没问题但前端调接口就是报403或者404。403大概率是拦截器把它拦截了404大概率是context-path没算进去。我们这个项目把context-path设成/api所以Controller里定义的路径是/recipe/list真实访问路径是/api/recipe/list前端基础URL务必要带/api。这个问题我在接手别人项目时遇到太多次了属于十分钟能查出来但没人愿意提前说的坑。4.5 日志跟踪与接口性能排查联调阶段出问题时我第一步永远是看日志而不是猜。这个项目的日志我分三层管理控制台日志开发时开启debug级别重点看SQL执行和参数绑定文件日志生产环境输出到服务器指定目录按天滚动接口访问日志用AOP切面统一记录每个接口的入参、出参和耗时。AOP记录接口耗时的切面长这样Aspect Component public class ApiLogAspect { Around(execution(* com.example.cooking.controller.*.*(..))) public Object log(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); long cost System.currentTimeMillis() - start; if (cost 1000) { log.warn(接口 {}.{} 响应耗时 {} ms, pjp.getTarget().getClass().getSimpleName(), pjp.getSignature().getName(), cost); } return result; } }我为什么要专门记录“超过1000ms”的告警日志因为在真实使用场景中网上能搜到的60%以上性能问题都可以通过这条日志定位到具体的慢接口然后再往下查是慢SQL还是慢外部调用。5. 项目收尾之后的几个实用建议这个厨艺交流平台写到这里其实已经覆盖了Spring Boot实战中最重要的几个面项目初始化、自动装配原理、注解实战、缓存使用、定时任务、接口鉴权、跨域处理和问题排查。如果你完整跟下来会发现它不只是一堆CRUD堆叠而是围绕真实业务场景做了一系列有意识的技术选型和架构决策。我做完这个项目后最深刻的一个体会是项目能否出彩拼的从来不是谁用了更多新技术而是谁对每个技术点的“适用边界”理解得更清楚。比如Redis不是拿来凑热闹的而是因为菜谱详情确实有高并发读压力JWT不是听别人说好就上而是因为前后端分离架构下Session确实不方便版本不选最新的是因为JDK 8和Spring Boot 2.7在这个项目场景下容错率最高。这些“为什么”能讲清楚面试官或者答辩老师对你的评价会完全不一样。最后再分享一个执行层面的小技巧每写完一个模块我建议立即用Swagger做一次完整的冒烟测试不要等到所有模块写完再统一测。因为功能越早验证定位bug的成本越低。另外代码里的一些工具类Redis操作封装、统一返回结果、全局异常处理尽量在项目初期就沉淀好后面每个模块的开发速度都会明显加快。按照这个节奏推进这个平台的完整开发周期大约在两周左右如果是全职做一周加两三个晚上就能跑通全流程。
返回列表