SpringBoot宠物服务平台实战:从自动装配到Vue部署全复盘

发布时间:2026/10/4 13:55:33
SpringBoot宠物服务平台实战:从自动装配到Vue部署全复盘 我先把这次做的东西交代清楚一个用SpringBoot搭出来的宠物服务平台不是什么集团级大项目但麻雀虽小五脏俱全从用户端到管理端、从登录鉴权到消息通知、从前端打包到部署上线全链路都是我自己一个人磕出来。这篇文章就按我实际动手的顺序复盘一遍该给的代码给代码该讲的坑讲坑尤其那些热搜里反复出现的SpringBoot高频问题比如自动装配、CGLIB代理、版本选择、Vue打包塞进Boot这类我会全部串到项目里说清楚。无论你是拿这个方向做毕设还是想把它写成简历里的实战项目这篇都可直接抄作业。1. 项目从哪来宠物店的预约需求怎么变成了一套系统先说背景。我朋友开了一家宠物洗护寄养店日常业务看着简单实际上全是纸和微信来回倒腾用户要提前约洗护时间店里要根据宠物档案提醒疫苗、驱虫时间寄养期间要记录喂养指令客户回访还要翻聊天记录。他找我帮忙时我第一反应是这不就是个小程序预约嘛真坐下来梳理才发现真正费时间的反而是管理端——店员要查档案、改订单状态、批量通知客户没有一个统一的流转入口用表格管迟早出事。所以我给这个平台划定的核心需求如下用户端注册登录、宠物档案管理、服务预约、订单查询、留言评价。管理端宠物信息管理、订单处理、寄养记录、客户回访列表、基础数据统计。辅助能力疫苗/驱虫定期提醒、预约前自动通知、搜索关键词分词处理。平台最终跑在SpringBoot上数据存储用MySQL缓存用Redis前端是Vue打包后直接放进SpringBoot的静态目录里。选这套组合的理由很现实第一朋友店里的部署环境就是一台普通云服务器内存不大不适合把前后端分离搞成两个独立服务再挂Nginx第二用户规模在可预见的未来就是几百个老客户单机SpringBoot完全撑得住不需要一上来就上微服务第三这套组合的知识密度高对于做毕设或者练手的人来说正好覆盖了三层架构、鉴权、消息、定时任务这些常见面试考点。项目结构我按Maven标准父子模块设计但实际上是单个Application没有拆成多模块。理由很简单单体应用拆多模块会增加类加载和依赖管理成本这个规模拆了纯属给自己找事。目录组织如下pet-service/ src/main/java/com/pet/ config/ // Spring配置类含WebMvcConfig、RedisConfig controller/ // 用户端与管理端接口 service/ // 业务逻辑层 mapper/ // MyBatis Mapper接口 entity/ // 数据库实体 dto/ // 接口入参出参对象 common/ // 统一返回体、异常、常量 util/ // JWT、加密、日期工具 src/main/resources/ mapper/ // XML文件MyBatis的SQL都在这 static/ // 前端Vue打包产物后端不用独立部署前端 application.yml这里有个我早期踩过的坑刚开始图省事把SQL注解直接写在Mapper接口上结果稍微复杂一点的联表查询就写得非常痛苦而且改动SQL要重新编译。后来老老实实全部改成XML方式才体会到好处。对宠物档案这种字段多、查询条件复杂的业务XML里写动态SQL配合where、if标签管理起来远比注解舒服。这也是为什么热搜里那么多SpringBootMyBatis的项目都要专门讲整合因为很多人跌在第一步的建表和管理方式上。2. 工程骨架与技术选型为什么死磕SpringBoot 2.7.x2.1 版本选择2.7.x是当前兼容性最稳的长期版本我不是没考虑过直接用SpringBoot 3.x毕竟它才代表新。实际动手后放弃了核心原因是整合成本。3.x默认基于JDK17我开发的机器是JDK8朋友服务器上的环境也是JDK8想跑3.x就得先给服务器装新JDK这对一个门店项目来说属于无意义的额外风险。更关键的是MyBatis官方对SpringBoot 3.x支持的starter版本号体系和2.x不一样Redis、Thymeleaf等配套依赖也跟着变了。网上一搜就知道很多人问SpringBoot版本太高怎么办问的往往就是3.x带来的系列兼容问题。我把当时的选型对比整理成了表格方便你判断自己的项目该选哪个对比项SpringBoot 2.7.xSpringBoot 3.x基础JDKJDK8/11可跑用户环境友好强制JDK17及以上MyBatis整合用mybatis-spring-boot-starter 2.x/3.x均可需单独适配需用mybatis-plus最新版或其3.x starter第三方中间件兼容大量老项目生态兼容好部分老版驱动或国产库驱动需升级新特性支持无法使用虚拟线程等JDK19能力支持newVirtualThreadPerTaskExecutor等新特性适合场景毕业设计、中小型单体、快速上线新项目、团队已用JDK17、追求长期维护最后我选了SpringBoot 2.7.18这个版本是2.x系列的最终维护版本安全性修复持续到2023年底以后相当长一段时间对个人项目来说已经闭眼可跑。项目结束后我专门看了一眼3.2的新虚拟线程执行器确实诱人如果你的环境是JDK21且没有老依赖包袱可以直接上3.2以后版本把spring.threads.virtual.enabled打开就能让Tomcat用虚拟线程并发能力对这类IO密集型项目提升明显。这个选项对我这次项目没用上但我把它记在升级路线里了。2.2 Java Maven项目构建依赖锁版本的三点经验Maven构建是最常见的Java项目构建方式项目初始化我直接用Spring Initializr生成比手动加依赖快得多。生成后第一时间做三件事锁版本、定仓库、跑通最小启动。锁版本的意思是别靠SpringBoot的BOM自动推导所有依赖版本比如MySQL驱动、Druid连接池、JJWT这些不在Boot管理范围内的库必须显式写version否则今天能启动明天突然报类冲突排查成本极高。定仓库则是针对国内网络环境的实操经验。我在pom里配置了阿里云Maven镜像下载依赖速度快了几倍IDE里也不再频繁卡在索引上。跑通最小启动更为关键——我拿到生成工程后的第一个动作是直接启动看到SpringBoot启动成功后再逐步加依赖。很多人喜欢一次性把MyBatis、Redis、安全框架全加进去结果第一个报错根本不是业务问题而是依赖冲突或驱动加载顺序这种开局地狱完全没必要。Maven构建还涉及一个高频困扰打包。SpringBoot Maven插件默认用spring-boot-maven-plugin的repackage目标生成可执行jar但如果你同时想生成普通jar给别的模块依赖就得小心配置。我在这个项目里就用到了skiptrue和classifier的配合确保部署包是Boot可执行jar而不是普通jar。以前见过同事把Boot项目打成jar后传到服务器上java -jar执行结果直接提示没有主清单属性十有八九就是没有用SpringBoot插件做repackage或者主类没配置对。2.3 IDEA里配置启动端口与Banner环境命名的坑话题回到开发环境。热搜里有IDEA 2026 怎么配置SpringBoot服务 编辑配置数据 比如启动端口这个问题其实答案是固定的——还是那套SpringBoot的端口配置优先级。在application.yml写server.port: 8081或者启动命令带--server.port8081都可以。IDEA的Spring Boot Run Configuration里可以在Program arguments填--server.port8081也可以在Environment variables里设置SERVER_PORT8081效果等效。我的习惯是配置多环境文件而不是只靠一个application.yml# application.yml spring: profiles: active: dev # application-dev.yml server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/pet_service username: root password: 123456 # application-prod.yml server: port: 8080 spring: datasource: url: jdbc:mysql://你自己的云数据库地址:3306/pet_service username: pet_user password: 这里填生产库密码这个做法避免了一个经典问题开发联调时端口被同事或自己另一个项目占用改配置文件还容易提交到Git上污染生产配置。多环境文件配合spring.profiles.active切换彻底把本地环境和部署环境分开了。Banner这块我个人建议改一下SpringBoot启动时那个大字母图案。技术上非常简单在resources目录放一个banner.txt写入你要的ASCII Art字符画直接覆盖。在线生成器随便搜都有搜springboot banner在线生成就行。很多人问这玩意儿有什么用——其实没什么硬性作用但每次启动看到自定义Banner会让项目显得是自己的项目而不是随手生成的Demo。面试聊到细节时这也算一个用心的小点。2.4 依赖清单与实际用途不是越多越好最终pom里的核心依赖和用途我列一下这也是给不想下太多无用依赖的同学一个参考。数据库用MyBatisMySQL驱动Druid连接池权限用JWTjava-jwt库轻量够用缓存用Redis消息用ActiveMQ工具链有Lombok、Hutool、Apache HttpClientSpringBoot本身还自带了AOP和Validation不需要额外加。至于Spring Cloud、Nacos这些这个项目完全用不上一个单体项目加微服务全家桶除了拖慢启动和增加面试被追问的暴露面没有任何好处。这些依赖的版本我当时花了大半天核对因为踩过一次SpringBoot版本太高引发的坑SpringBoot 2.7.x配了Druid 1.2.8以上没问题但MyBatis用2.3.x会出现SLF4J绑定重复警告。所以我的经验是Boot 2.7项目里MyBatis和Druid凑成一套黄金组合不要随便看着官网版本号往上跳稳定能跑比新功能重要。3. 自动装配与MyBatis整合注册登录和宠物档案从哪里开始长出来3.1 SpringBoot自动装配原理面试高频点也是项目起点一旦开始往工程里加依赖就绕不开为什么SpringBoot加个starter就能自动帮你创建DataSource、SqlSessionFactory这个问题。这是SpringBoot自动装配的工作。原理并不玄乎每个starter包里的META-INF下会有spring.factories文件SpringBoot 2.7时代或者/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件新版里面列举了一堆xxxAutoConfiguration类名。SpringBoot启动时通过EnableAutoConfiguration导入这些类再依靠ConditionalOnClass、ConditionalOnProperty、ConditionalOnMissingBean这些条件注解决定哪个配置类生效。举个例子MyBatis的自动配置类是MybatisAutoConfiguration。它生效的前提是classpath里存在SqlSessionFactory和SqlSessionFactoryBean这两个类并且容器里还没有用户自定义的同名Bean。所以我在写项目的时候偶尔想覆盖某些默认行为比如自定义数据源最简单的方式就是声明一个自己的DataSourceBean自动配置里的ConditionalOnMissingBean看到你已经定义了就不会再帮你创建默认的。理解这个机制后配置数据库、Redis这类组件就不再是照着抄配置而是知道每一步为什么会被加载。3.2 注册与登录模块MyBatis写了个最简单的账号系统用户注册是宠物服务平台的入口我用MyBatis实现的最基础流程正好可以拆开讲Controller接收DTOService层校验和加密Mapper落库启动时自动扫描Mapper接口并注册到容器。创建一个Mapper接口Mapper public interface UserMapper { User selectByUsername(Param(username) String username); int insertUser(User user); }对应XML里写insert idinsertUser parameterTypecom.pet.entity.User useGeneratedKeystrue keyPropertyid INSERT INTO t_user (username, password, nickname, pet_count, create_time) VALUES (#{username}, #{password}, #{nickname}, #{petCount}, #{createTime}) /insert这里有个关键细节useGeneratedKeystrue配合keyPropertyid才能在插入后拿回数据库自增主键。很多第一次写的人踩过这个坑插入后用户对象的id一直是null但数据库中确实多了记录。我在写注册接口时就依赖这个id去初始化默认的宠物档案容器所以把这一行当成固定配置写熟了。密码存储用的是BCrypt这个通过简单加密就可以不要用MD5明文更是零容忍。Spring Security的crypto模块里就有BCrypt实现单独引这个小功能不必把整个Security框架拉进来。我写了个UserService做加密、查重、插入三步每个操作划到一个事务里。在SpringBoot中给方法加Transactional很方便但小细节要注意只有public方法且通过代理对象调用时事务才生效同类内部调用this.另一个方法()会跳过事务代理这是CGLIB代理模式下非常典型的隐性坑。3.3 宠物档案的实体设计与MyBatis映射宠物档案表是这个平台的业务核心因为后面的预约、寄养、疫苗提醒全部围绕它展开。我建表时的主要字段有宠物ID、用户ID、宠物昵称、品种、性别、生日、体重、体内芯片ID、疫苗到期日、驱虫到期日、备注。字段不算多但查询组合复杂例如管理端要筛选某天之前需要打疫苗的犬类客户或者最近一周新登记的猫咪这种SQL用XML写动态条件最简单。select idselectPetArchives resultTypecom.pet.dto.PetArchiveDTO SELECT p.*, u.nickname AS owner_name FROM t_pet p LEFT JOIN t_user u ON p.user_id u.id where if testpetName ! null and petName ! AND p.pet_name LIKE CONCAT(%, #{petName}, %) /if if testspecies ! null and species ! AND p.species #{species} /if if testvaccineDueBefore ! null AND p.vaccine_due_date lt; #{vaccineDueBefore} /if /where /select与MyBatis整合时最常遇到的问题不是SQL本身而是实体字段与数据库列名对不上。我的数据库列名是下划线风格create_timeJava实体的属性是驼峰createTime因此在application.yml里必须开启下划线自动转驼峰配置mybatis: configuration: map-underscore-to-camel-case: true若不加这一行查询结果返回时所有下划线字段全部为null表面上代码没报错但数据不对这种错排查起来最费时间。另一个高频问题是resultType和resultMap的选择单表简单查询用resultType自动映射没问题涉及联表、嵌套对象就必须定义resultMap否则查询结果还是对的但你拿不到预期的嵌套结构。宠物档案和管理端订单列表我全用resultMap明确字段映射关系也方便以后加字段。4. 登录态与接口安全为什么SpringBoot默认用CGLIB代理会坑到你的拦截器4.1 从Session到JWT给宠物店管理端加个无状态登录用户端登录后后续每次请求怎么识别身份最传统的办法是Session服务端保存登录状态客户端拿着Cookie。项目早期我确实用了Session但很快发现两个问题第一服务端重启后Session全部丢失用户全被踢下线第二如果后端接口将来要给小程序用小程序对Cookie的支持并不友好。所以我切换成了无状态JWT方案做法是登录成功后服务端签发一个Token返回前端前端后续请求都带Authorization: Bearer token头后端解析Token识别用户身份。Token签发与校验我封装在JwtUtil工具类里。签发时我设置了三部分信息用户ID、用户名、角色普通用户还是管理端。有效期设为7天管理端Token给2天。JWT的过期时间是通过payload里的exp声明传递的校验时用HMAC256签名验证密钥单独放在application-prod.yml里不写进代码仓库这是安全底线。public String generateToken(Integer userId, String username, String role) { Algorithm algorithm Algorithm.HMAC256(secretKey); return JWT.create() .withClaim(userId, userId) .withClaim(username, username) .withClaim(role, role) .withExpiresAt(new Date(System.currentTimeMillis() expireTimeMs)) .sign(algorithm); }4.2 拦截器实现与登录白名单有了Token就要有统一校验入口。SpringBoot里最直接的方式是自定义一个HandlerInterceptor注册到WebMvcConfigurer里。我把登录校验、管理端权限校验放在同一个拦截器中通过URL前缀区分接口权限级别。逻辑很简单从请求头取出Token校验签名和过期时间再把解析出的用户信息塞进请求Attribute后续Controller里通过RequestAttribute取值就不需要每个接口重复查一遍用户表。public class LoginInterceptor implements HandlerInterceptor { 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); } // 解析失败或过期时直接返回401 try { DecodedJWT jwt JwtUtil.verify(token); request.setAttribute(currentUserId, jwt.getClaim(userId).asInt()); request.setAttribute(currentRole, jwt.getClaim(role).asString()); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或token已过期\}); return false; } } }这里必须留白名单注册、登录、首页公开信息接口不能拦截。注册拦截器时用addPathPatterns表示拦截哪些路径用excludePathPatterns表示排除哪些路径。我一开始把/static/**忘了排除结果前端Vue页面在登录前根本加载不出来这个问题排查了整整半个小时才意识到是拦截器把静态资源也拦了。这个错误非常典型值得所有做Boot的人记住。4.3 CGLIB代理默认值拦截器与Service的关系把话题再深入一点。SpringBoot默认使用CGLIB动态代理这个问题等价于Boost代理策略里的默认选择从Spring Boot 1.4开始SpringBoot的官方starter基本把代理设置为proxyTargetClasstrue也就是对类进行代理而非对接口代理。为什么因为JDK动态代理只能代理接口但开发者大量依赖第三方库和业务实现类很多Bean根本没有接口强行JDK代理就会启动报错。SpringBoot为了减少用户配置默认CGLIB代价就是代理方式和Spring原生略有不同。这个默认值对项目的实际影响主要有两个方向。一个就是前面提到的this内部调用绕过代理事务、缓存、异步注解全部失效。另一个影响是某些最终类final class无法被CGLIB增强Spring会明确报错final class cannot be proxied。我在定时任务里就遇到了把某个发送通知的Service类不小心写成了final启动时直接炸。这类问题与业务无关但极其影响推进速度所以写SpringBoot项目时我养成了习惯凡是会被AOP增强的类尽量避免加final修饰符。4.4 接口签名与幂等防止管理端被人乱调登录之外的另一个安全话题是接口签名认证。宠物店的H5页面、未来可能对接的第三方合作商会通过开放接口调后端我不能让这些客户端直接暴露裸接口否则改个参数能把订单价格改掉。做法是约定一个签名规则发起方拼接参数加上双方共享的secretKey做MD5/HMAC256服务端按同样规则计算后比对。同时加入时间戳参数并校验有效期例如5分钟内有效防止重放攻击。这部分代码核心在拦截器中逻辑不复杂却在面试中能讲出我是有安全意识的亮点。签名的计算我建议统一封装成工具类签名原文的值序列化必须固定用TreeMap保证参数顺序否则两端因参数顺序不一致永远签名对不上。这个细节我调试了很久因为后端用的TreeMap天然排序前端如果按参数天然顺序拼接一两个参数看着没问题参数一多就开始随机失败。5. 消息、定时任务与扩展点疫苗提醒和预约通知是怎么自动跑起来的5.1 Scheduled定时任务与线程池宠物平台最容易被感知的自动化能力是疫苗/驱虫到期提醒。每天早上9点扫一遍宠物档案找出到期日前后3天内的记录给用户发送站内信或微信模板消息。我用的是SpringBoot自带的定时任务框架启动类上加EnableScheduling然后为每个定时业务写一个方法标注Scheduled(cron表达式)。Component public class VaccineRemindTask { Resource private PetMapper petMapper; Resource private NotifyService notifyService; Scheduled(cron 0 0 9 * * ?) public void sendVaccineRemind() { ListPetArchive duePets petMapper.selectDueVaccine(DateUtil.offsetDay(new Date(), -3), DateUtil.offsetDay(new Date(), 3)); for (PetArchive pet : duePets) { notifyService.sendVaccineRemind(pet); } } }真正的坑是默认线程池。SpringBoot的Scheduled若不显式配置线程池默认是单线程执行所有任务。这意味着只要某一个定时任务执行时间较长其他任务会排队严重时会延后到错误时间触发。我在项目里加了一个自定义调度线程池配置把线程数调到4并设置了拒绝策略丢弃旧任务而不是抛异常。同时注意定时任务不要设计成执行时间跨分钟的长任务如果确实要跑多次分页查询任务内部自己维护分页游标别一把梭查全表。5.2 异步通知与ActiveMQ不要让用户等待微信推送预约成功或取消时需要给用户发通知。这类通知走外部HTTP接口延迟不可控如果同步执行用户预约接口的RT会额外增加几百毫秒体验很差。我引入了SpringBoot的Async异步方法把通知发送放进独立的线程池。唯一要记住的是入口方法必须由外部调用同一个类内部调用this.asyncMethod()无效因为代理没有经过Spring容器这又是CGLIB代理的一个衍生问题。再往后走一步就是消息队列。为什么一个小项目也要用ActiveMQ其实初衷很简单寄养订单状态变化涉及多个动作——改档案、发通知、记日志、更新统计用同步事务把所有动作串起来一个环节慢了全链路卡住。我用ActiveMQ作为事件总线订单状态变更发布一条消息消费者分别处理通知和统计。选ActiveMQ而不是RabbitMQ或Kafka的理由是足够轻SpringBoot提供了spring-boot-starter-activemqJmsTemplate发消息只需三行代码对于这种低频业务完全够用。用Kafka或Flink去处理这个规模的数据属于典型的杀鸡用牛刀虽然热搜里有很多SpringBoot整合Flink的题但真实项目里应该按数据量说话先把ActiveMQ用好。jmsTemplate.send(pet.order.remind, session - session.createTextMessage( JSON.toJSONString(new RemindMessage(orderId, customerPhone, type)) ));5.3 文本搜索与语音备注HanLP和ASR接口的实用场景项目管理端每天会收到若干条宠物洗澡的备注例如胆小洗澡时需主人陪同背部有皮肤病注意吹干。这些文本是非结构化数据想按关键词搜索怎么办我接到一个宠物评论/留言的情感倾向分析需求时第一步想到的不是什么大模型而是HanLP分词库。在SpringBoot项目中直接引入HanLP的轻量包可以对中文备注做分词、关键词提取、情感倾向打分。宠物店铺的评价留言里我把情感分按阈值分成积极和消极两类管理端就能直观看到哪些服务让客户不满意。ListString keywords HanLP.extractKeyword(content, 3); double sentiment SentimentAnalyzer.analyze(content);另一个听起来高级但平时用得上的功能是语音备注。店员在忙的时候经常没空打字我用SpringBoot封装了一个调用ASR语音识别接口的模块上传音频文件到识别服务返回文本后存入备注字段这样店员就在现场念一句系统自动转成文字。这里有个技术点上传文件用RestTemplate发送MultipartFile不能直接把MultipartFile对象塞进去需要转成ByteArrayResource并设置正确的ContentType否则语音接口会报文件流错误。HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORMAT_DATA); MultiValueMapString, Object body new LinkedMultiValueMap(); body.add(file, new ByteArrayResource(file.getBytes()) { Override public String getFilename() { return file.getOriginalFilename(); } }); body.add(model_type, pet_service_v1); ResponseEntityString resp restTemplate.postForEntity(asrUrl, new HttpEntity(body, headers), String.class);5.4 定时任务与消息的稳定性幂等和重试提醒类任务的稳定性不能靠直觉。因为消息可能重复投递定时任务可能因为服务器重启而重复执行我在消费端一律做了幂等消息表按业务唯一键订单号、宠物ID提醒类型日期做唯一约束重复消息会被捕获后跳过。消费失败也必须有重试机制ActiveMQ可以配合消息的延迟级别做重发单纯靠try-catch加日志是解决不了网络抖动的。我在application-dev.yml里调大了连接超时和消费线程数生产环境则把预取限制调小避免批量拉取导致消息积压后同一时间全部超时。6. 前端Vue打包放进SpringBoot部署背后的文件和路由那些事6.1 Vue打包为什么要塞进后端宠物平台的用户端界面我用Vue写的管理端也是。开发时前后端分离各跑各的端口前端代理转发接口到后端。但部署时我没用Nginx原因前面说了服务器内存有限不想再把一个Nginx进程塞进去。SpringBoot天然支持把静态资源放在classpath:/static/下把Vue构建产物扔进去用户访问根路径就直接打开前端页面。对几百个用户的场景这种方式完全可行运维复杂度直接降为0。6.2 Vue打包路径与路由模式最容易翻车的两处Vue项目打包后资源路径默认是绝对路径/js/app.js这在后端根路径部署时没问题。但如果你的后端加了server.servlet.context-path或者在管理端使用了子路径资源路径就必须改成相对路径。修改vue.config.js中publicPath为./让打包出的index.html里引用的静态资源都变成相对路径这样无论放在根路径还是子路径都能正常加载。第二坑是路由模式。Vue Router的history模式会让前端路由看起来非常干净比如/order/123但问题是刷新时浏览器会直接向SpringBoot发这个路径的请求后端没有这个路由返回404。我处理的办法是开发时用hash模式避免这个问题如果需要history模式就加一个SpringBoot的WebMvcConfigurer把非接口路径全部转发到index.html这相当于在SpringBoot里模拟Nginx的try_files能力核心逻辑就是匹配不含.和api前缀的路径转发到/index.html。6.3 阿里云构建与部署从本地打jar包到服务器运行部署环节我在阿里云上做的是最常规但最稳妥的方案本地或流水线执行mvn clean package把target下生成的jar传到服务器然后nohup java -jar跑起来。这里必须区分两个jarpet-service-1.0.0.jar和pet-service-1.0.0.jar.original。前者是SpringBoot repackage后的可执行胖jar后者是没被SpringBoot插件改过的普通jar只能作为依赖库用。很多新手传错了jar启动时报no main manifest attribute就是这个原因。阿里云构建工具我用过一次配置起来本质上就是两步代码仓库拉源码、Maven构建产物自动上传到服务器。它的好处是以后在手机上都可以触发部署不用每次本地产出再手动scp。如果只是做个毕设或本地项目完全没有必要上这套CI/CD把mvn clean package和scp两个命令背熟就够了。6.4 上线后要盯的三个指标和两个常见启动问题服务上线后别急着关终端。第一次用java -jar启动时我遇到过两个经典报错。第一个是端口被占用因为服务器上还跑着别的项目端口8080被占了我通过lsof -i:8080查到进程然后改用--server.port8081启动。第二个是数据库连接失败原因是云数据库的白名单没加服务器IP这类远程连接问题在本地永远复现不了只能在服务器日志里看到Communications link failure对应处理方式是在云数据库控制台加白名单或检查用户名密码。日常运行中我重点盯三个指标接口响应时间、定时任务是否准点执行、消息队列积压量。宠物平台数据量小QPS低前面两个看日志就能观察个大概消息积压去ActiveMQ的控制台看queues里的Pending数量。可能有人觉得这种级别的项目不值得搞监控但从我几年做项目的经验来说任何上线系统都应该有可观察性的意识哪怕只是定一个每天扫一眼的日志巡检任务也能避免很多小问题拖成大事故。写在最后的项目复盘这个项目从需求梳理到上线跑通用了三周大部分时间花在三个地方表结构反复调整、前端路由与后端拦截器联调、部署阶段Druid连接池的时区问题。如果让我重新做一次我会先花半天把表结构和接口契约定清楚把常用词、必填字段、状态流转列成一张表贴在桌面而不是边写边改。另外我会更早地测试打包部署把本地环境跑通和可部署jar包能跑当成两个独立任务因为前者顺利并不能保证后者同样顺利。最后分享一个小技巧接手这类SpringBoot单体项目时不要急着改代码先把spring.factories和自动配置类相关的日志看一遍Started Application in x.xxx seconds之前的所有Conditional分析能帮你快速判断为什么某个Bean没创建、为什么某个配置没生效。这个习惯救过我很多次比瞎猜和断点调试效率高得多。