简介:一套基于Vue与SpringCloud的微服务博客系统完整源码及项目文档,面向具备Java基础、想系统掌握前后端分离与微服务架构的开发者,可帮助解决分布式项目中服务治理、网关路由、链路追踪等落地难题。压缩包共1025个文件,含265个java后端源码、360个js脚本、70个vue组件、55个xml配置及23个yml等,类型涵盖前后端编码、构建配置、说明文档与图表样式资源,总大小91.44MB。目前已有648人学习下载。项目覆盖Eureka高可用集群、Zuul网关、Feign负载均衡、Elasticsearch与Zipkin链路追踪等微服务核心组件,并整合Redis缓存与Redisson分布式锁、RabbitMQ延迟队列、Elasticsearch高亮分页、WebSocket实时通信及支付宝支付,支持Docker部署,采用前后端分离工程结构,从服务发现、网关路由到分布式协同均有可运行实现,代码注释完整、扩展方便,适合作为毕设、课程设计或全栈微服务学习的参考工程。
1. 从单体到微服务:这套 Vue+SpringCloud 博客系统的设计与实现
个人博客用微服务,乍一听是杀鸡用牛刀,但把它当成一个学习样本,价值就完全不一样。这套基于 Vue + SpringCloud 的博客系统,麻雀虽小五脏俱全:前端是 Vue 单页应用,后端按业务拆成用户、文章、评论三个服务,中间有 Gateway 网关统一入口,Redis 扛缓存,Nacos 做注册与配置中心。它比电商、秒杀这类大型项目简洁得多,但该有的微服务组件一个不少。
如果你正在做毕设、从 SSM 单体往 SpringCloud 过渡,或者前端想完整走一遍后端链路,这套资源的架构思路和代码落地方案都值得照着敲一遍。我把拆解过程中涉及的服务划分、参数配置、跨域处理和缓存动手实践全部整理出来,所有命令都是本机可复现的,踩过的坑也一并交代清楚。
2. 把博客拆成微服务:SpringCloud 组件选型与四个模块划分
2.1 为什么选 SpringCloud 而不是 SSM 单体:技术栈对比
先回答一个很多人会问的问题:博客这种内容型网站,单体能跑,微服务也能跑,凭什么用后者?我看过不少 springcloud 微服务开源项目,多数是抄来抄去的订单系统,业务逻辑复杂但讲解混乱,反而不适合入门。这套博客的好处是业务模型足够简单——文章、用户、评论,天然适合按业务边界切分。
从技术栈对比来看,结论更清楚:
| 维度 | SSM 单体 | SpringCloud 微服务 |
|---|---|---|
| 部署 | 一个 WAR 包 | 多个服务独立部署 |
| 数据库 | 单库单表 | 按服务分库 |
| 学习曲线 | 平缓 | 陡峭,需要理解注册中心、网关、负载均衡 |
| 适用场景 | 内容简单、访问量可控 | 需要横向扩展、服务拆分、团队协作 |
| 踩坑成本 | 低 | 高,网络、版本、配置都可能出问题 |
博客确实不需要微服务,但你需要通过一个熟悉的业务去理解微服务的协作方式。这类「入门简洁」的实践项目,恰恰是最合适的跳板。
2.2 模块怎么拆:一次请求的完整链路
这套博客系统拆了四个服务:网关服务(blog-gateway)、用户服务(blog-user)、文章服务(blog-article)、评论服务(blog-comment)。每个服务独立一个端口,数据库按服务拆分,用户表、文章表、评论表各归各的库。
一次请求在微服务环境下的完整链路是:浏览器发起请求,先打到 Gateway 网关,网关做 JWT 校验和路由转发;文章相关的请求被转发到文章服务,文章服务查 Redis 缓存,缓存未命中再查数据库;页面需要显示评论时,文章服务通过 Feign 调用评论服务,拿到评论列表组装后返回前端。用户登录则是先到网关,再转发到用户服务,用户服务校验账号密码后签发 JWT。
这样拆的好处是每个服务只关心自己的表,代码边界清晰,团队协作时互不干扰。坏处也很明显——你在本地要同时启动 Nacos、Gateway 和三个服务,机器配置差一点就会卡。我一般建议至少 16G 内存再跑全套。
2.3 基础设施搭建:Nacos 注册中心与配置中心
服务注册与发现用的是 Nacos,既当注册中心又当配置中心,这是目前 SpringCloud 微服务开源项目里最常见的选型。相比 Eureka,Nacos 自带控制台和配置管理,对新手友好很多。
先加入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.1</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2021.1</version> </dependency>版本号要和 SpringBoot 对应。我本地用的是 SpringBoot 2.6.x 配 SpringCloud Alibaba 2021.1,这套组合经过大量项目验证,坑最少。
然后配置地址:
spring: application: name: blog-article cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yamlapplication name是注册到 Nacos 的服务名,网关根据这个名字做路由转发。server-addr是 Nacos 的地址,本地默认 8848 端口,如果你改了端口,这里要同步改。
Nacos 启动用 standalone 模式:
startup.cmd -m standalone启动后访问http://localhost:8848/nacos,默认账号密码都是 nacos,记得第一次登录后改掉。很多人在注册中心翻车,多半是 Nacos 没启动就把服务启动了,或者 namespace 不一致导致服务找不到——这个问题后面避坑章节专门写。
3. Vue 前端落地:路由参数、代理配置与打包进 Spring Boot
3.1 环境配置与项目初始化
前端部分依赖 Node.js 环境。vue 安装及环境配置这一步,最常见的问题不是不会装,而是装完 node 之后 npm 命令不生效,或者版本太新导致 Vue CLI 安装失败。我一般用 nvm 管理 Node 版本,稳一点。
node -v npm -v npm install -g @vue/cli vue create blog-frontendvue create会进入交互式命令行,选择手动配置,勾选 Router 和 Vuex。如果网络不好,npm 装依赖卡住,可以切换淘宝镜像源,但公司内网环境请确认是否允许使用第三方镜像。
安装依赖常报错,我遇到最多的是node-sass编译失败,原因是 Node 版本与 node-sass 版本不匹配。现在的新项目建议直接用sass替代node-sass,免编译,省心很多。
3.2 路由设计:动态路由与参数传递
博客前端路由只有四个核心页面:首页文章列表、文章详情、登录页、个人中心。麻雀虽小,但 vue 路由参数该有的细节一个不少。
// router/index.js const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/article/:id', name: 'ArticleDetail', component: () => import('@/views/ArticleDetail.vue'), props: true }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/user/:username', name: 'UserCenter', component: () => import('@/views/UserCenter.vue'), props: true }, ]/article/:id是动态路由,:id对应文章 ID,页面里通过this.$route.params.id拿参数。props: true可以让路由参数直接作为组件 props 传入,组件内部不用再依赖$route,可测试性更好。
从列表页跳到详情页时有个天然坑:如果用户从/article/1点击推荐位跳到/article/2,Vue 会复用同一个组件实例,created钩子不会重新执行,页面内容不更新。解决方法是给router-view加key,或者监听$route变化重新拉取数据:
watch: { '$route.params.id'(newId) { this.fetchArticle(newId) } }3.3 跨域与代理:vue.config.js 的 devServer
开发阶段前端跑在 8080 端口,后端网关跑在 8080 端口,直接请求必然跨域。常规做法是在 vue.config.js 里配置 devServer 代理。
// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }target指向网关地址,changeOrigin必须为 true,否则后端收到的请求头里Host还是前端域名。pathRewrite把前端的/api前缀去掉再转发,网关侧就不用额外处理前缀。
开发模式下前端代码里写axios.get('/api/article/list'),实际请求会被代理转发到http://localhost:8080/article/list。上线后这段代理配置不生效,需要靠 Nginx 做反向代理,这是后面打包部署的重点。
3.4 打包放进 Spring Boot:dist 目录与其他路径的关系
vue 打包放进 springboot 中这个操作,看起来只是把目录复制过去,实际操作坑不少。执行构建命令:
npm run build产物在dist目录下。把dist下的所有文件复制到网关服务(或其他静态资源服务)的src/main/resources/static目录里,重新打包网关服务即可。
注意三个前提。第一,publicPath 必须是相对路径:
module.exports = { publicPath: './', outputDir: 'dist', assetsDir: 'static' }publicPath写成'./',打包出来的资源引用才是相对路径,否则部署到子路径下所有静态资源全部 404。第二,路由使用 history 模式时,Spring Boot 需要把未知路径转发到 index.html,否则刷新页面直接 404。第三,如果后端接口有 context-path,前端代理和 Nginx 的 location 都要同步调整,三处不一致就翻车。
4. 后端服务细节:Gateway 鉴权、Feign 调用与文章服务
4.1 网关层:JWT 校验与白名单
网关是微服务的门面,所有请求先进这里做统一鉴权。博客场景下用 JWT 无状态校验,比 Session 更适合微服务,因为用户服务不需要维护会话状态,网关校验通过后把用户 ID 放进请求头转发到下游服务即可。
@Component public class AuthFilter implements GlobalFilter { private static final List<String> WHITE_LIST = Arrays.asList("/auth/login", "/article/list", "/article/detail"); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (!JwtUtil.validate(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } }白名单放行登录、文章列表、文章详情这类不需要登录的接口,其余请求强制校验 token。JWT 校验失败返回 401,前端 axios 拦截到 401 后跳转登录页。
注意 JWT 的密钥不要硬编码在代码里,要放到 Nacos 配置中心,环境不同用不同密钥。还有 token 过期时间,博客场景一般设置 7 天,但如果是练习项目,2 小时更接近真实业务。过期时间写太长,后面调试权限问题时你会怀疑人生。
4.2 文章服务:分页查询与统一返回结构
文章服务是核心,所有服务统一返回结构,前端处理起来才不费劲。
@RestController @RequestMapping("/article") public class ArticleController { @Autowired private ArticleService articleService; @GetMapping("/list") public R<PageResult<ArticleVO>> list( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String categoryId) { PageResult<ArticleVO> page = articleService.pageQuery(current, size, categoryId); return R.ok(page); } }pageQuery内部用 MyBatis-Plus 的分页插件,底层是分页拦截器,自动拼接LIMIT语句。current是页码,size是每页条数,这两个参数必须做上限控制,否则用户传一个size=10000会把数据库打爆。categoryId是可选参数,用来做分类筛选。
统一返回结构R里包含 code、message、data 三部分,code 为 0 表示成功,非 0 表示各种业务异常。前端 axios 响应拦截器统一判断 code,省去每个请求单独处理的重复劳动。
4.3 评论服务与 Feign:跨服务拿用户名
评论是拆出来的独立服务,但前端展示评论时需要显示用户名,用户名存在用户服务里。跨服务获取数据有几种做法,这里选 Feign 调用。
@FeignClient(name = "blog-user", fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/user/info/{id}") R<UserInfoVO> getUserInfo(@PathVariable("id") Long userId); }name是目标服务的注册名,Feign 内部通过负载均衡从 Nacos 找到服务实例。评论服务拿到评论列表后,根据 userId 的集合调用一次这个接口批量换取用户名和头像,然后组装返回。
这里有个性能坑要提醒:千万不要在 for 循环里一条一条调用 Feign,评论有 100 条就发 100 次请求,服务必然被拖垮。正确做法是接口设计成批量查询,一次传多个 ID。另外 Feign 调用的超时时间默认很短,本地调试稍慢就超时,配置里要放宽连接超时和读取超时。
feign: client: config: default: connectTimeout: 5000 readTimeout: 50004.4 登录与令牌:JWT 无状态方案
登录流程:前端把用户名密码发给网关,网关转发到用户服务,用户服务校验通过后生成 JWT 返回前端,前端存到 localStorage 或 cookie。后续请求在 header 里带上Authorization: Bearer token,网关校验签名与过期时间。
JWT 方案对比 Redis 存登录态,优势是网关不需要回源查 Redis,多实例部署也天然支持。劣势是 token 无法主动吊销,用户改密码后旧 token 仍然有效,直到过期。博客这种低安全敏感场景完全够用,但做项目实践时可以思考如何用 Redis 黑名单列表解决这个缺陷——这也是面试官喜欢问的点。
用户密码存储用 BCrypt 加密,就算数据库泄露也不会直接暴露明文。注册接口写好后,记得做密码强度校验,全数字化密码在真实场景会被刷爆。
5. 常见问题避坑:跨域、注册、缓存与端口之争
5.1 排错顺序与日志入口
微服务排错比单体难,因为一个请求跨多个服务,问题可能发生在任意环节。我自己的排错顺序是固定的:先看浏览器 Network 面板请求是否发出去、状态码多少;然后看网关日志有没有进;再看目标服务日志有没有打;最后看数据库慢查询和 Redis 命中情况。这套顺序每次都能快速缩小范围,省去乱猜的时间。
网关日志里TraceId要打通到下游服务,建议接一个日志链路追踪组件。如果暂时没接,至少在各服务日志里统一打印请求路径和用户 ID,否则定位跨服务问题就是在黑匣子里摸。
5.2 前端请求 403:CORS 与 OPTIONS 预检
现象:前端请求后端接口,浏览器控制台报 CORS 错误,请求根本没发出去就是 403。 原因:浏览器同源策略拦截,跨域请求会先发 OPTIONS 预检,后端没正确处理预检请求。 解决:在网关层写一个 CORS 过滤器,允许来自前端域的跨域请求,并对 OPTIONS 请求直接放行。
@Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:8080"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }allowedOrigin不要写成*,因为开启allowCredentials时浏览器不允许*作为来源。生产环境这个配置也建议按域名精确配置,否则任何网站都能跨域调用你的接口。
5.3 Nacos 注册不上:namespace、版本与网络
现象:服务启动成功,日志也打了 register,但 Nacos 控制台看不到服务。 原因:最常见的三个,namespace 不一致、服务端口没对外开放、SpringCloud Alibaba 版本与 Nacos 服务端版本不兼容。 解决:先确认三个服务的spring.cloud.nacos.discovery.namespace配置一致,默认是 public;再看防火墙是否放行 8848 端口;最后检查 Nacos 服务端版本,2.x 服务端配老版本客户端会出现注册成功但心跳失效的玄学问题,建议服务端 2.2.0 以上配客户端 2021.1 以上。
注册不上去是最费时间的坑,因为服务启动看起来正常,也没有异常堆栈,就是列表里找不到。排查时打开 Nacos 客户端日志,看到 heartbeat 正常才说明真的注册上了。
5.4 Redis 缓存穿透:并发请求打爆数据库
现象:首页文章列表接口慢得离谱,数据库 CPU 飙升,但 Redis 里明明有缓存。 原因:缓存 key 设计不合理。比如查询不存在的文章 ID,Redis 没有缓存,请求全部穿透到数据库,大量恶意请求直接压垮数据库。 解决:对空值也做缓存,缓存时间设置短一些,同时做参数校验,非法 ID 直接拒绝。更稳的方案是加布隆过滤器,所有请求先过布隆过滤器判断 ID 是否可能存在,不存在的直接返回。
缓存空值虽然简单,但要注意设置过期时间,否则大量不存在的 key 堆积在 Redis 里,内存迟早被耗光。
5.5 Vue 部署后刷新 404:history 路由的兜底
现象:前端打包放进 Spring Boot 后,访问首页正常,点击链接跳转也正常,但刷新详情页变成 404。 原因:前端用 history 模式,浏览器直接请求/article/1,后端没有这个路由,返回 404。 解决:网关层加一个转发规则,把非 api 开头的路径都转发到 index.html,交给前端路由处理。
spring: cloud: gateway: routes: - id: frontend uri: no://local predicates: - Path=/** filters: - RewritePath=/.*, /index.html这个配置要放在最后作为兜底路由,前面的 api 路由优先匹配。Nginx 部署也一样,try_files $uri $uri/ /index.html;一行解决。
6. 阅读量计数的优化:Redis 计数与定时落库的完整闭环
博客的文章详情页有阅读量,最简单的实现是每次访问update article set view_count = view_count + 1 where id = ?,但并发一上来,这条 update 会频繁锁行,严重拖慢详情页响应。我的做法是 Redis 计数、定时落库。
每次浏览详情时INCR article:view:{id},详情接口直接读 Redis 这个 key 返回阅读量。这样数据库一条 update 都不用发,Redis 单线程处理 INCR,天然原子,并发再高也不会出现计数丢失。后台再挂一个定时任务,每 5 分钟把这段时间的增量刷回数据库。
@Scheduled(cron = "0 */5 * * * *") public void flushViewCount() { Set<String> keys = redisTemplate.keys("article:view:*"); for (String key : keys) { String articleId = key.split(":")[2]; Integer increment = redisTemplate.opsForValue().get(key); if (increment != null && increment > 0) { articleMapper.increaseViewCount(articleId, increment); redisTemplate.delete(key); } } }这段逻辑的关键是INCR命令本身返回值是自增后的值,我这里是每 5 分钟读取一次再清空重计。更稳妥的做法是用 ZSET 或者队列做增量持久化,防止服务崩溃丢失 5 分钟内的计数,但博客场景这个精确度足够了。
验证这一步容易被忽略,个人博客系统测试不能只测功能通不通,并发行为必须验证。我一般 launch 一个 JMeter 压 100 个线程循环看详情页,压完比对 Redis 总计数和数据库落库数据是否一致,几次下来数据一致才算通过。
这套方案我接手过三次,前两次都在落库时机上栽过跟头——定时任务没加分布式锁,多个实例同时跑就重复计数。从那以后,我每次做这类缓存写回功能都强制走一遍并发压测和落库校验,自己确认过数据无损再上线,希望帮到你。
本文还有配套的精品资源,点击获取