开头部分
前后端分离的商城项目,这几年一直是Java全栈学习路上的标配。我之前带学员做毕设时,发现很多同学一上来就选SpringBoot + Vue,但真正能把SpringBoot3和Vue3从零串起来跑通一个完整商城的,少之又少。这次我把自己做鲜花商城系统的完整思路和带敲过程整理出来,从技术选型到数据库设计,从后端接口到前端页面,再到部署后经常踩的坑,一条线讲清楚。这个项目不复杂,但麻雀虽小五脏俱全,适合正在学SpringBoot3、Vue3,或者准备做毕业设计、求职项目的同学参考。跟着敲一遍,你不仅能理解前后端如何配合,还能避开我踩过的那些坑。本文所有代码基于实际可运行方案,我会把关键配置和参数计算过程也讲明白,不是那种贴一堆代码但不解释的伪教程。
1. 项目整体设计与技术选型
1.1 为什么选SpringBoot3 + Vue3这套组合
先说大背景。SpringBoot3在2022年底正式发布,底层基于Spring Framework 6和Jakarta EE规范,最大的变化是javax包名换成了jakarta,并且强制要求JDK17起步。很多老教程还在用JDK8,直接照搬会报编译错误。Vue3则从2020年发布以来,组合式API和响应式重构带来的类型推断体验,已经全面超越Vue2。如果你现在还新起项目用Vue2,说实话有点逆版本了。选这套组合,一方面是为了跟上前端工具链的节奏,另一方面是为了在简历上能写“新版全栈技术栈”,而不是还在啃十年前的老框架。
但SpringBoot3并不是简单的版本升级。它默认使用Jakarta Servlet规范,很多老项目的拦截器、过滤器写法都要改。比如javax.servlet要换成jakarta.servlet,这个细节最容易坑到第一次用SpringBoot3的人。另外,SpringBoot3的自动配置也做了精简,比如spring.factories被AutoConfiguration.imports取代。我后面会演示一个完整的项目配置,让你少走弯路。
Vue3这边,我选的是Vite作为构建工具。Vite启动速度快,热更新秒级响应,比Webpack体感好太多。Vue3的核心优势是组合式API,能把同一业务逻辑的代码聚在一起,而不是像Options API那样散落在data、methods、computed里。尤其是商城这种涉及商品列表、购物车、订单状态多个模块联动的东西,组合式API写起来清爽得多。
1.2 鲜花商城系统的核心业务模块
先别急着写代码,画一张业务脑图。鲜花商城说到底还是电商,核心链路是“浏览商品 → 加入购物车 → 生成订单 → 模拟支付 → 查看订单状态”。我设计的这个系统包含五个核心模块:用户模块(注册、登录、信息管理)、商品模块(花材分类、商品列表、商品详情)、购物车模块(加入、修改数量、删除、选中结算)、订单模块(提交订单、取消订单、发货模拟、订单详情)、以及一个简单的后台管理模块(商品上架、订单处理)。你不要觉得后台管理多余,实际开发中运营人员必须有一个地方维护鲜花商品,不然数据全写在数据库里没法玩。
每个模块我尽量做得适中,不贪多。比如权限这块,我只做JWT登录校验,不做Spring Security那套复杂配置。很多人喜欢一上来就整合Security+JWT,结果配置配了两天,核心业务还没开始。对于学习型项目,用一个拦截器校验JWT就足够了,等理解了认证流程再升级Security也不迟。
1.3 技术栈明细表
我把整个项目的技术栈整理成一张表,方便你对照查漏。
| 端 | 技术选型 | 说明 |
|---|---|---|
| 后端 | JDK17 + SpringBoot3.1.x | 长期支持版本 |
| 后端 | MyBatis-Plus 3.5.x | 简化CRUD,自带分页插件 |
| 后端 | MySQL 8.0+ | 数据库,注意字符集utf8mb4 |
| 后端 | JWT(jjwt 0.11.x) | 无状态登录令牌 |
| 后端 | Lombok | 减少样板代码 |
| 后端 | Hutool | 常用工具类,省时间 |
| 前端 | Vue3.2 + Vite4 | 组合式API + script setup |
| 前端 | Vue Router4 + Pinia | 路由与状态管理 |
| 前端 | Axios | HTTP请求库 |
| 前端 | Element Plus | 电商后台页面样式组件 |
这里要说明一下,为什么不用Redis做购物车?因为学习型项目里,把购物车存在前端状态和本地存储,逻辑更直观,也更容易调试。真实商城里购物车肯定要落库,但那是分布式会话的方案,初学者先掌握单体场景即可。等到你做高并发项目,再引入Redis存购物车、存验证码,那是后话。
2. 环境准备与项目初始化
2.1 后端SpringBoot3工程的创建方式
创建SpringBoot3项目推荐用Spring Initializr(start.spring.io),而不是自己手敲pom依赖。打开官网后,Project选Maven,Language选Java,Spring Boot版本选3.1.x的最新3.1版本(我当时用3.1.5)。Java版本必须选17。Dependencies里勾上Spring Web、MySQL Driver、Lombok。生成后解压,用IntelliJ IDEA打开。
这里我要专门说一下JDK版本问题。SpringBoot3最低要求JDK17,但JDK21已经出来了,你本地装JDK17或JDK21都可以。我推荐JDK17,因为很多企业生产环境还在用17,跑起来兼容性最稳。如果你电脑上装了多个JDK,记得检查IDEA的Project Structure里Project SDK是不是17,还有Maven的JRE设置是不是指向了17。我遇到过有的人代码没问题,但Maven默认用了JRE8,编译直接报错invalid target release: 17,其实就是Maven用错了JDK。
pom文件里,我额外加了MyBatis-Plus和JWT依赖。MyBatis-Plus版本要注意,SpringBoot3需要引用mybatis-plus-spring-boot3-starter,而不是旧版的mybatis-plus-boot-starter。这个坑特别典型,旧版starter会加载不到自动配置,导致Mapper扫描不到,启动报错。下面是关键的pom片段:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>注意,jjwt的api、impl、jackson三者版本必须一致,否则运行时会报NoClassDefFoundError。这个属于经验问题,很多人抄依赖只抄了api,一调用Token解析直接崩。
2.2 前端Vue3工程初始化
前端我用Vite创建项目。执行命令:
npm create vite@latest flower-shop-frontend框架选择Vue,然后选择TypeScript还是JavaScript。这里我建议:如果是你一个人做毕设,选JavaScript就好,学习成本低,写起来快。如果你还想练一手TypeScript,那选TypeScript,但后面接口类型定义会让你多花时间。我教程里用JavaScript,方便你看代码逻辑。
创建完成后,安装必要依赖:
npm install npm install vue-router@4 pinia axios element-plus安装Element Plus时,我建议采用全量引入方式,对于后台管理项目不用过度优化,全量引入后组件直接用,省得按需加载还要配unplugin-vue-components。全量引入在主入口main.js里加两行:
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css'Vite配置里我加了端口和代理,先把server配置好。因为前后端分离开发时,前端端口5173,后端端口8080,直接请求会有跨域问题。代理配置写死在vite.config.js里:
export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, } } } })这里你可能会问,为什么/api代理到后端,但后端接口我之前没说过要加/api前缀?对,所以我规划后端接口统一以/api开头,这样前端代理和后端请求路径就对齐了。在SpringBoot里,你可以通过server.servlet.context-path=/api来设置统一前缀,也可以在每个Controller上加@RequestMapping("/api/xxx")。我个人倾向于用context-path,省事且不会漏。
2.3 数据库设计:花材表与订单表字段规划
数据库名字就叫flower_shop,字符集用utf8mb4,排序规则utf8mb4_general_ci。因为鲜花商品描述里可能存Emoji表情,utf8mb4能存四个字节的字符,如果用utf8mb3,插入Emoji直接报错。这是实际开发里一个经典坑。
表结构我设计了一下,核心四张表:flower_user用户表、flower_category分类表、flower_product商品表、flower_order订单表。另外购物车不建表,因为上文说了购物车数据放前端本地存储。你可以用下面DDL建表,我做了一些简化,保证学习够用。用户表关键字段:
CREATE TABLE `flower_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'MD5加密后的密码', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `address` varchar(255) DEFAULT NULL COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;商品表的字段要注意价格用decimal(10,2),别用float。float在比较金额时会出精度问题,比如显示99.9,实际是99.900000000001。银行计算金额会用Decimal类型,Java里对应BigDecimal。商品表:
CREATE TABLE `flower_product` ( `id` bigint NOT NULL AUTO_INCREMENT, `category_id` bigint NOT NULL COMMENT '分类ID', `name` varchar(100) NOT NULL COMMENT '鲜花名称', `cover` varchar(255) DEFAULT NULL COMMENT '封面图URL', `detail` text COMMENT '详情描述', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int NOT NULL DEFAULT 0 COMMENT '库存', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表我加了状态字段,用tinyint存。0待支付、1已支付待发货、2已发货、3已完成、4已取消。这个状态机是商城的灵魂,后面讲接口时会细说。订单表和订单明细表我合并成一张了,省去外键关系,学习项目用一张表存商品快照和数量。字段里product_name、product_price、quantity是从商品表拷贝过来的快照,这样商品改了价格或名称后,历史订单仍能显示当时购买的信息。这个设计思路叫“读写分离”,跟数据库读写分离不是一回事,但很重要。
3. 后端核心功能实现详解
3.1 SpringBoot3整合MyBatis-Plus的配置细节
启动类上记得加@MapperScan,不然Mapper接口扫描不到。这是最基础的一步,但仍然有人忘记。你可以把@MapperScan直接写在启动类上,指定mapper包路径:
@SpringBootApplication @MapperScan("com.example.flowershop.mapper") public class FlowerShopApplication { public static void main(String[] args) { SpringApplication.run(FlowerShopApplication.class, args); } }配置文件application.yml里,JdbcUrl要注意加上useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。之前有人忘了加characterEncoding,结果中文乱码。完整的配置类似:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/flower_shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai另外,MyBatis-Plus的逻辑删除和自动填充功能,我用在了订单表上。逻辑删除开关配置在application.yml里:
mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0逻辑删除的好处是不物理删除,数据还在,方便做报表和分析。你可能会说,我订单表用了逻辑删除吗?我在订单表加了一个deleted字段,用户取消订单时,实际上不删记录,只是把状态改成已取消。所以逻辑删除字段只在需要物理删除的场景用,比如删除购物车记录(如果购物车落库的话)。这里先放着,你知道有这功能即可。
3.2 JWT登录认证与拦截器的实现思路
我不打算引入Spring Security,只为拦截器加一个简单的JWT校验。首先,登录接口校验用户名密码,然后生成Token。这里密码我用了MD5加盐方式,虽然MD5不算安全,但学习项目够用。你如果想进阶,可以用BCrypt,但需要额外引依赖。这里我演示JWT的核心流程,而不是加密算法。
生成Token的工具类里,我用HmacSHA256签名算法,密钥固定写死一个不少于32字节的字符串。注意,JWT要求HS256密钥长度至少256位(32字节),如果你用太短的字符串,JJWT会报错Keys must be at least 256 bits long。很多人第一次写JWT就栽在这。下面是一个简化版工具类片段:
public class JwtUtil { private static final String SECRET = "flower-shop-secret-key-please-change-me-123456"; private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }拦截器实现HandlerInterceptor,在preHandle里从请求头Authorization前缀Bearer后面取Token,解析失败就返回401。注册拦截器时要排除登录、注册接口。这里有个细节:SpringBoot3里WebMvc自动配置类不能用WebMvcConfigurerAdapter,那是SpringBoot2的老写法。你要实现WebMvcConfigurer接口,代码:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/user/login", "/api/user/register"); } }拦截器里我还会顺便把解析出来的userId放到request.setAttribute中,后续controller可以直接从request中取,避免每次查询用户信息:
request.setAttribute("userId", claims.get("userId"));这样设计以后,订单接口里可以直接Long userId = (Long) request.getAttribute("userId"),不需要前端传用户ID,也防止越权操作。
3.3 商品列表与购物车接口设计
商品列表接口返回分类以及对应商品列表,前端首页只需要调一个接口就能拿到所有数据。我设计了两个接口:GET /api/category/list返回所有分类,GET /api/product/list?categoryId=xxx按分类查商品。如果你懒,可以直接写GET /api/product/list返回所有商品,前端自己过滤。但做商城还是建议后端就分好类,减少前端工作。
商品列表返回的数据结构我用了统一响应类Result,所有接口都返回Result.success(data)或Result.error(msg)。这样前端只要在Axios响应拦截器里统一处理code即可。Result类里我定义code(200成功,500失败)、msg、data三个字段。
购物车因为放前端,所以后端只提供一个结算时校验库存的接口。前端把购物车数据放在Pinia里,购物车数据结构类似:
{ products: [ { productId: 1, name: '香槟玫瑰', price: 99.00, quantity: 2, checked: true } ] }操作购物车时,前端本地完成增删改,不需要请求后端。这里最需要注意的是数量边界判断。我见过不少同学写的数量减到-1,这就是没有做下限判断。在Vue的方法里,你应该这样写:
const decreaseQuantity = (item) => { if (item.quantity <= 1) { ElMessage.warning('鲜花数量至少为1') return } item.quantity-- }后面我会在常见问题里专门讲这个坑。
3.4 订单流程中的状态机设计
订单状态是整个商城最需要严谨的部分。从“待支付”到“已支付待发货”再到“已发货”“已完成”或者“已取消”,每一步都应该有状态转换的校验,不能跳步。比如一个“待支付”订单,你不能在没支付时直接给它改成“已发货”。我在订单接口里实现了这个校验逻辑,每次更新订单状态前,先判断当前状态与目标状态是否符合流转规则。
具体代码里,我定义了一个转换Map:
private static final Map<Integer, List<Integer>> ORDER_STATUS_TRANSITIONS = new HashMap<>() {{ put(0, Arrays.asList(1, 4)); // 待支付 -> 已支付 / 取消 put(1, Arrays.asList(2, 4)); // 已支付 -> 已发货 / 取消(晚于发货前) put(2, Arrays.asList(3)); // 已发货 -> 已完成 put(3, Collections.emptyList()); }};提交订单的接口流程如下:先从请求里取出商品列表和数量,遍历商品计算总金额,并扣减库存。扣库存时注意,要在更新SQL里加判断stock >= quantity,否则并发下会超卖。MyBatis-Plus的Update方法支持条件构造器:
boolean success = productMapper.update(null, new LambdaUpdateWrapper<Product>() .eq(Product::getId, productId) .ge(Product::getStock, quantity) .setSql("stock = stock - " + quantity)) > 0;如果更新影响行数为0,说明库存不足,直接返回错误,并回滚整个事务。这里我用@Transactional保证扣库存和插入订单在同一个事务里,避免扣了库存但订单没生成的情况。
生成订单时,我还要把商品快照写入订单表中。表字段已经设计过了,直接取值即可。支付接口呢,我模拟了一下,直接调用订单服务将状态从0改成1。现实中要对接微信支付或支付宝,会涉及回调签名校验,这里不展开。
4. 前端页面与交互实现
4.1 路由与页面结构
前端页面我分了几个视图:Login.vue、Register.vue、Home.vue(商品分类和商品列表)、ProductDetail.vue、Cart.vue、OrderConfirm.vue、OrderList.vue、以及后台的AdminProducts.vue和AdminOrders.vue。路由配置中,我加了路由守卫,未登录用户只能访问登录注册页和首页,下单时必须已登录。
Vue Router4的守卫写法与Vue2略有不同:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })这里有个细节:为什么token存localStorage而不是sessionStorage?因为localStorage跨标签页共享,关闭浏览器后再次打开仍保持登录态。sessionStorage标签页之间独立,刷新页面还有,但重新开标签页就没了。商城系统一般希望用户下次打开还保持登录,所以用localStorage。缺点是XSS攻击能偷走token,但学习项目不用过度防御。
页面结构我采用后台管理常用的侧边栏+顶栏布局。首页是电商风格的顶部导航和商品网格,用Element Plus的el-card和el-image组件展示花图片。后台管理用el-table展示商品和订单数据,方便运营操作。
4.2 商品展示与购物车状态管理
商品列表页面里,我用Pinia定义购物车store。Pinia相比Vuex,没有了mutations,直接在store里定义actions修改state,写法更舒服。cart.js文件:
import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ items: JSON.parse(localStorage.getItem('cartItems') || '[]') }), getters: { totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0), totalPrice: (state) => state.items.reduce((sum, item) => { return sum + (item.checked ? item.price * item.quantity : 0) }, 0) }, actions: { addItem(product) { const existing = this.items.find(item => item.productId === product.id) if (existing) { existing.quantity++ } else { this.items.push({ productId: product.id, name: product.name, price: product.price, cover: product.cover, quantity: 1, checked: true }) } this.save() }, removeItem(productId) { this.items = this.items.filter(item => item.productId !== productId) this.save() }, save() { localStorage.setItem('cartItems', JSON.stringify(this.items)) } } })注意,getters里的totalPrice我加了一个checked判断,只有选中的商品才合计金额。前端有这个判断后,用户取消勾选商品,底部结算金额会自动变化。这个细节在实现时很容易漏,漏了就会导致“取消勾选后价格不变”的问题。
加入购物车的按钮事件要调用cartStore.addItem(product),然后提示用户。这里我建议在加入购物车后不跳转购物车页,而是弹个提示“已加入购物车”,用户自然会去点击购物车图标。这个交互更符合电商习惯。
4.3 封装Axios请求与统一拦截
前端对接后端,axios要封装一下。我在src/utils/request.js里创建axios实例:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求错误') return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { ElMessage.error('网络请求异常') } return Promise.reject(error) } ) export default request这里有一个容易踩的坑:我在响应拦截器里直接返回了res.data,也就是说业务代码拿到的数据已经解包了。如果后端返回结构是{code, msg, data},那么调用接口时你拿到的就是data部分。这样做确实方便,但会让调试变得不直观,因为控制台打印出来的是数据对象,而不是完整响应。我建议在代码里写清楚注释,避免后续维护的人困惑。
登录接口调用:
const res = await request.post('/user/login', { username, password }) localStorage.setItem('token', res.token)注意baseURL:'/api'配合Vite代理,在开发环境没问题。生产部署时,你要把前端的/api也通过Nginx反代到后端服务,这样跨域问题就消失了。
5. 常见问题与排查实录
5.1 Vue3项目部署后报Uncaught SyntaxError: Unexpected token
这个问题在知乎上问的人特别多,我也遇到过。现象是本地开发正常,但把打包后的dist放到Nginx上,打开页面控制台报Uncaught SyntaxError: Unexpected token '<'。原因通常是资源路径加载错误,浏览器请求JS文件时,服务端返回了index.html而不是JS文件。解决方案有两步:
第一,Vite打包配置里要设置base: './',否则默认base是/,部署在子路径下就会找错资源。在vite.config.js中加:
export default defineConfig({ base: './', // ... })第二,Nginx里要配置try_files,确保找不到文件时回退到index.html,这样才能支持Vue Router的history路由。但如果你用hash路由,就不需要这个。我在项目中用了createWebHistory,所以Nginx配置写:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }另外,如果你把静态资源放到了CDN或对象存储,base路径还要做相应调整。总之遇到Unexpected token,先看控制台请求的JS路径对不对,再检查base配置。
5.2 接口跨域与Session失效问题
开发时我把前后端分离,Vite代理已经解决了跨域,但有些同学会直接在前端用http://localhost:8080发请求,就会触发跨域。解决方案最推荐前端开代理,其次是在SpringBoot里配置CORS跨域。我不建议在SpringBoot里配置全局CORS,因为安全起见,用代理更合适。但如果你的接口要给第三方系统调用,那才需要CORS配置。
还有一个Session失效问题,如果前后端分离,后端使用HttpSession保存登录状态,那么每次前端请求必须带Cookie,而Vite代理默认changeOrigin: true,并且浏览器同源策略下Cookie会被正确处理。但如果你后端配置了server.servlet.session.cookie.same-site或者跨域,Cookie很可能携带不了。索性我用JWT,Session无关痛痒。
5.3 Vue3中购物车数量不能小于0的处理
这个热词很有代表性。很多人在写quantity++或quantity--时,没有做边界控制。在组合式API中,如果你用reactive对象数组,直接修改元素的属性是响应式的。比如:
const decrease = (item) => { if (item.quantity > 0) { item.quantity-- } }但这样写有个隐患:用户点减号到0之后,物品还留在购物车里,数量为0却还占着位置。实际场景中,数量为0时应该自动删除商品,或者至少提示“不能再减少”。我更推荐的写法是:
const decrease = (item) => { if (item.quantity === 1) { cartStore.removeItem(item.productId) ElMessage.success('已移出购物车') } else { item.quantity-- } }这里你根据自己的业务决定。如果希望不能小于1,那就用前面第3.3节的写法;如果希望可以直接移除,就用这种。两种都行,但必须做一个,不能留着负数。
5.4 SpringBoot3日志配置logback-spring.xml要点
SpringBoot3默认用Logback日志框架,如果你不配置,控制台日志也能看,但生产环境最好配置输出到文件,并且按天滚动、压缩。这里我结合热搜词“springboot3 logback-spring.xml”来说一下。先附加一个依赖,其实SpringBoot已经内置了Logback,不需要额外引。在resources下新建logback-spring.xml,里面配置一个ConsoleAppender和一个RollingFileAppender。需要注意一点:SpringBoot的logging.config默认会自动加载logback-spring.xml,不要用logback.xml,因为logback-spring.xml支持springProfile标签,可以在不同环境输出不同日志级别。
一个简化的配置:
<configuration> <include resource="org/springframework/boot/logging/logback/base.xml"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/flower-shop.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/flower-shop.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>30</maxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <logger name="com.example.flowershop" level="DEBUG"/> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </configuration>我在实际项目里还发现一个点:如果日志文件路径下的目录不存在,Logback不会自动创建目录吗?其实是会自动创建的,但前提是应用启动时对目录有写权限。部署到Nginx服务器上,如果用户不对,启动应用的用户没有logs目录权限,就会报错。生产上我喜欢给日志文件加按天和按大小双滚动的策略,便于排查线上问题。
结尾
这个鲜花商城项目,我前前后后带过好几批学员,每次都会遇到新的小坑。尤其是SpringBoot3刚出来那阵子,网上资料混杂,一会儿javax一会儿jakarta,一会儿JDK8一会儿JDK17,很多人卡在启动阶段就放弃了。实际上,只要把环境版本统一好,数据库和前端以最简单的方式跑通,后面的业务代码并不难。我个人在带敲过程中感触最深的一点是:不要为了追求技术新颖而疯狂引入中间件,先把一个订单流程从商品到下单一气呵成跑通,比什么高并发、分布式都重要得多。如果你正在学习这两个框架,建议一定亲手把项目敲一遍,别只看不练。代码敲到哪有问题,就在哪里停下来调试,这个过程里的成长,比看十遍教程都有效。最后再分享一个实用小技巧:前端接口联调时,打开Chrome开发者工具的Network面板,看每个请求的响应体,接口有问题先看状态码和响应内容,能排查掉90%的问题。希望这个项目能成为你全栈之路的一个好跳板。