1. 项目概述:从单体应用到前后端分离的必然之路
刚入行那会儿,做Java Web项目还是JSP、Freemarker的天下,前端代码和后端逻辑搅在一起,改个按钮颜色都得重新打包部署整个应用,效率低得让人抓狂。后来Ajax流行起来,算是有了点分离的苗头,但本质上还是后端控制着视图渲染。直到近几年,前端框架像Vue、React的成熟,加上后端SpringBoot这种“约定大于配置”的框架普及,真正的前后端分离才成了现代Web开发的标准姿势。
这个“SpringBoot+Vue入门并实现前后端分离和数据库查询”的项目,说白了,就是带你亲手搭一个最经典的现代Web应用骨架。它要解决的核心痛点很明确:让前端工程师专心写页面交互,让后端工程师专注业务逻辑和数据处理,双方通过清晰的API接口契约协作,互不干扰,并行开发,提升整体交付效率和质量。这个项目非常适合刚学完Java基础和Vue基础、想看看它们怎么配合的初学者,或者是从传统单体应用转型过来的开发者。通过这个项目,你能摸清楚一个请求从前端Vue组件发出,到后端SpringBoot控制器接收、处理、查询数据库,再返回数据给前端渲染的完整闭环。别看流程简单,这里面涉及的环境搭建、跨域处理、API设计、联调技巧,每一个都是实战中必踩的坑。
2. 技术栈选型与项目初始化:为什么是它们?
2.1 后端:SpringBoot,不仅仅是简化配置
选择SpringBoot作为后端框架,远不止因为它能帮我们省去一大堆XML配置。对于这个入门项目而言,它的核心优势在于“开箱即用”和“快速聚焦”。
SpringBoot Starter依赖是精髓。我们不需要再纠结该引入哪些版本的Spring MVC、Jackson、Tomcat。只需要在pom.xml里声明一个spring-boot-starter-web,它就会自动帮我们引入一个能直接运行Web应用的所有必要依赖,并且这些依赖的版本都是经过兼容性测试的,极大避免了依赖冲突这个“新手杀手”。对于数据库操作,我们选择spring-boot-starter-data-jpa配合MySQL驱动。JPA(Java Persistence API)是一种规范,Hibernate是其最流行的实现。为什么不用更灵活的MyBatis?对于入门项目,JPA的Repository接口和方法名派生查询能让我们几乎不写SQL就完成基础的增删改查,非常适合快速验证业务模型。你只需要定义一个继承JpaRepository的接口,Spring Data JPA就能自动实现常见的数据操作。
注意:很多新手会在
spring-boot-starter-data-jpa和mybatis-spring-boot-starter之间纠结。我的建议是,入门期优先用JPA,它的“约定大于配置”能让你更快理解ORM(对象关系映射)的思想。等熟悉了基本操作,再根据项目对复杂SQL的需求去学习MyBatis。
项目初始化,我强烈推荐使用 Spring Initializr (IDEA内置了它的界面)。在勾选依赖时,除了必选的Spring Web和Spring Data JPA,记得把MySQL Driver也选上。生成项目后,用IDEA打开,第一件事就是配置application.yml(或application.properties)。
# application.yml 配置示例 server: port: 8080 # 后端服务端口 spring: datasource: url: jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 启动时根据实体类自动更新表结构,生产环境请勿使用! show-sql: true # 在控制台打印SQL语句,调试神器 properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect # 指定方言这里有个关键参数ddl-auto,update模式在开发时非常方便,Hibernate会自动检查实体类与数据库表的差异,并执行修改。但严禁在生产环境使用,因为它可能导致数据丢失。生产环境应该使用validate或none,并通过规范的SQL脚本管理表结构。
2.2 前端:Vue 3 + Vite,体验现代前端开发流
前端选择Vue 3,不仅因为其简洁的API和强大的响应式系统,更因为其组合式API(Composition API)提供了比Vue 2选项式API更好的逻辑复用和组织能力。虽然对于第一个项目你可能感觉不明显,但早点接触组合式API对未来开发复杂组件有益无害。
脚手架工具我们不用传统的Vue CLI,而是用Vite。Vite的优势在于极快的冷启动和热更新。它利用浏览器原生ES模块导入,在开发环境下不需要打包,瞬间启动。对于新手来说,更快的反馈循环意味着更高的学习效率和更少的等待烦躁感。
使用npm或yarn初始化Vue项目:
npm create vue@latest # 或 yarn create vue在创建向导中,项目名称(如vue-frontend)、是否启用TypeScript、JSX、Vue Router、Pinia(状态管理)、ESLint等,可以根据需要选择。对于纯入门项目,Vue Router(用于页面路由)和Pinia(用于状态管理)建议先不选,以减少初始复杂度,聚焦在核心的“请求-响应”流程上。ESLint可以选择,它有助于保持代码规范。
创建完成后,进入项目安装依赖并启动:
cd vue-frontend npm install npm run dev看到localhost:5173(Vite默认端口)可以访问,说明前端环境好了。
2.3 前后端分离的目录结构规划
在IDEA中,一个清晰的目录结构能有效管理前后端代码。我推荐两种方式:
单仓库并列结构(适合个人学习/小项目):
my-fullstack-demo/ ├── backend/ # SpringBoot项目根目录 │ ├── src/ │ ├── pom.xml │ └── ... └── frontend/ # Vue项目根目录 ├── src/ ├── package.json └── ...将两个项目放在同一个父目录下,方便同时打开和切换。IDEA可以打开
my-fullstack-demo作为根项目,然后通过“添加模块”的方式把backend和frontend都加进来,但注意前端模块可能需要配置为JavaScript/TypeScript模块。完全独立仓库(推荐,更符合工程实践): 后端(
springboot-backend)和前端(vue-frontend)分别是两个独立的Git仓库,放在两个独立的IDE窗口或编辑器中开发。这种方式职责清晰,部署独立,更贴近真实团队协作场景。
对于入门,我建议先用第一种,管理起来简单直观。
3. 后端核心实现:构建RESTful API与数据层
3.1 定义数据实体(Entity)与Repository
假设我们做一个最简单的“用户信息”查询功能。首先在backend/src/main/java/com/example/demo/entity下创建User实体类。
package com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; @Data @Entity @Table(name = "user") // 指定对应数据库表名 public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增 private Long id; @Column(nullable = false, unique = true) // 非空且唯一 private String username; @Column(nullable = false) private String email; // 省略构造器、getter/setter,使用了Lombok的@Data注解自动生成 }这里用了@Data注解,它是Lombok提供的,能自动生成getter、setter、toString等方法。需要在pom.xml中引入Lombok依赖,并且IDE需要安装Lombok插件才能正常识别。
接着,创建Repository接口。在backend/src/main/java/com/example.demo/repository下创建UserRepository。
package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface UserRepository extends JpaRepository<User, Long> { // 方法名派生查询:根据用户名查找 User findByUsername(String username); // 查找所有邮箱包含特定字符串的用户 List<User> findByEmailContaining(String keyword); }你不需要写这个接口的任何实现类。Spring Data JPA会在运行时动态生成实现。JpaRepository已经提供了save(),findAll(),findById(),deleteById()等基本CRUD方法。上面自定义的findByUsername,JPA会根据方法名自动解析并生成查询SELECT * FROM user WHERE username = ?,这就是“方法名派生查询”的魅力。
3.2 编写服务层(Service)与控制器(Controller)
虽然对于简单查询,可以直接在Controller中调用Repository,但引入Service层是一个好习惯,它负责业务逻辑,使Controller更专注于处理HTTP请求和响应。
在service包下创建UserService:
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { @Autowired private UserRepository userRepository; public List<User> getAllUsers() { return userRepository.findAll(); // 调用JPA内置方法 } public User getUserByUsername(String username) { return userRepository.findByUsername(username); // 调用自定义方法 } public List<User> searchUsersByEmail(String keyword) { return userRepository.findByEmailContaining(keyword); } }最后,也是最关键的一环:RESTful API控制器。在controller包下创建UserController。
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController // 表明这是一个RESTful控制器,返回值直接序列化为JSON @RequestMapping("/api/users") // 定义API的基础路径 public class UserController { @Autowired private UserService userService; @GetMapping // 对应 GET /api/users public List<User> getAllUsers() { return userService.getAllUsers(); } @GetMapping("/{username}") // 对应 GET /api/users/{username} public User getUserByUsername(@PathVariable String username) { return userService.getUserByUsername(username); } @GetMapping("/search") // 对应 GET /api/users/search?email=xxx public List<User> searchUsersByEmail(@RequestParam String email) { return userService.searchUsersByEmail(email); } }@RestController:组合了@Controller和@ResponseBody,意味着每个方法的返回值都会通过Spring的HttpMessageConverter(默认是Jackson)自动转换成JSON格式写入HTTP响应体。@RequestMapping:定义类级别的请求映射路径。@GetMapping:指定处理HTTP GET请求。@PathVariable:将URL路径中的变量(如{username})绑定到方法参数。@RequestParam:将HTTP请求参数(如?email=xxx)绑定到方法参数。
至此,一个简单的查询API就完成了。启动SpringBoot应用(运行DemoApplication里的main方法),访问http://localhost:8080/api/users,如果数据库有数据,你应该能看到返回的JSON数组。
3.3 跨越“天堑”:解决跨域问题(CORS)
当前端项目运行在localhost:5173,后端在localhost:8080,端口不同,浏览器出于安全考虑会阻止这种跨域请求。这是前后端分离遇到的第一个,也是最常见的“拦路虎”。
解决方式是在SpringBoot后端进行CORS配置。创建一个配置类WebConfig:
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 对哪些API路径启用CORS .allowedOrigins("http://localhost:5173") // 允许的前端源 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的HTTP方法 .allowedHeaders("*") // 允许的请求头 .allowCredentials(true); // 是否允许发送Cookie等凭证 } }; } }这个配置告诉浏览器,来自http://localhost:5173的对/api/**路径的请求是安全的,允许访问。在生产环境中,allowedOrigins应该替换为确切的前端部署域名,而不是*(允许所有源),这是重要的安全实践。
4. 前端核心实现:Vue组件与异步请求
4.1 安装Axios并封装请求工具
Vue项目本身不处理HTTP请求,我们需要一个HTTP客户端库。Axios是基于Promise的、功能丰富的HTTP库,是Vue生态中的首选。
在前端项目根目录下安装Axios:
npm install axios为了统一管理API请求(比如统一处理错误、设置基础URL),我们通常不会在每个组件里直接axios.get(...),而是进行一层封装。在src目录下创建utils文件夹,然后创建request.js:
// src/utils/request.js import axios from 'axios'; // 创建一个自定义的Axios实例 const request = axios.create({ baseURL: 'http://localhost:8080', // 后端API的基础地址 timeout: 5000, // 请求超时时间(毫秒) }); // 请求拦截器(可选):在发送请求前做点什么,例如添加token request.interceptors.request.use( (config) => { // 假设我们把token存在localStorage const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器(可选):在收到响应后做点什么,例如统一处理错误 request.interceptors.response.use( (response) => { // 如果后端返回的数据结构是 { code: 200, data: ..., message: 'success' } // 可以在这里直接返回 response.data.data return response.data; }, (error) => { console.error('API请求错误:', error); // 可以在这里根据HTTP状态码或自定义错误码进行统一错误提示 if (error.response && error.response.status === 401) { // 未授权,跳转到登录页 window.location.href = '/login'; } // 将错误继续抛给具体的请求调用处处理 return Promise.reject(error); } ); export default request;接着,创建src/api目录来管理所有API接口。创建userApi.js:
// src/api/userApi.js import request from '@/utils/request'; // @ 代表 src 目录,需要在vite.config.js中配置 export function getAllUsers() { return request({ url: '/api/users', method: 'get', }); } export function getUserByUsername(username) { return request({ url: `/api/users/${username}`, method: 'get', }); } export function searchUsersByEmail(emailKeyword) { return request({ url: '/api/users/search', method: 'get', params: { email: emailKeyword }, // Axios会自动将params拼接到URL上 }); }4.2 构建Vue组件并调用API
现在我们来创建一个显示用户列表的组件。在src/components下创建UserList.vue。
<template> <div class="user-list"> <h2>用户列表</h2> <!-- 搜索框 --> <div class="search-box"> <input type="text" v-model="searchKeyword" placeholder="输入邮箱关键词搜索..." @input="handleSearch" /> </div> <!-- 加载状态 --> <div v-if="loading">正在加载用户数据...</div> <!-- 错误信息 --> <div v-if="error" class="error">{{ error }}</div> <!-- 用户列表 --> <ul v-else-if="users.length > 0"> <li v-for="user in users" :key="user.id"> {{ user.username }} - {{ user.email }} </li> </ul> <div v-else>暂无用户数据</div> </div> </template> <script setup> // 使用Vue 3的组合式API import { ref, onMounted } from 'vue'; import { getAllUsers, searchUsersByEmail } from '@/api/userApi'; // 定义响应式数据 const users = ref([]); // 用户列表数据 const loading = ref(false); // 加载状态 const error = ref(''); // 错误信息 const searchKeyword = ref(''); // 搜索关键词 // 获取所有用户的函数 const fetchUsers = async () => { loading.value = true; error.value = ''; try { const response = await getAllUsers(); users.value = response; // 根据后端实际返回数据结构调整,这里假设直接返回数组 } catch (err) { error.value = '获取用户列表失败:' + (err.message || '未知错误'); console.error(err); } finally { loading.value = false; } }; // 搜索用户的函数(使用防抖优化,避免频繁请求) let searchTimer = null; const handleSearch = () => { clearTimeout(searchTimer); searchTimer = setTimeout(async () => { if (!searchKeyword.value.trim()) { fetchUsers(); // 关键词为空,恢复显示全部 return; } loading.value = true; try { const response = await searchUsersByEmail(searchKeyword.value.trim()); users.value = response; } catch (err) { error.value = '搜索失败:' + err.message; } finally { loading.value = false; } }, 300); // 延迟300毫秒执行 }; // 组件挂载时加载数据 onMounted(() => { fetchUsers(); }); </script> <style scoped> .user-list { padding: 20px; } .search-box { margin-bottom: 15px; } .search-box input { padding: 8px 12px; width: 300px; border: 1px solid #ccc; border-radius: 4px; } .error { color: red; margin: 10px 0; } ul { list-style: none; padding: 0; } li { padding: 8px; border-bottom: 1px solid #eee; } </style>这个组件演示了几个关键点:
- 组合式API (
<script setup>):逻辑更集中,ref定义响应式数据,onMounted处理生命周期。 - 异步请求:使用
async/await配合Axios调用封装的API函数,代码更清晰。 - 状态管理:用
loading、error、users等ref管理UI状态。 - 用户交互:通过
v-model绑定搜索框,@input监听输入事件,并实现了简单的防抖(setTimeout/clearTimeout)来优化性能。 - 条件渲染:使用
v-if、v-else根据状态显示不同内容。
最后,在src/App.vue中引入并使用这个组件:
<template> <div id="app"> <UserList /> </div> </template> <script setup> import UserList from './components/UserList.vue'; </script>启动前端(npm run dev)和后端,访问http://localhost:5173,如果一切顺利,你应该能看到从后端数据库查询并渲染出来的用户列表,并且搜索功能也能正常工作。
5. 联调、部署与进阶思考
5.1 前后端联调实战技巧与问题排查
即使代码都写对了,第一次联调也难免遇到问题。这里有几个必查清单:
网络问题(最基础也最易忽略):
- 检查服务是否启动:分别访问
http://localhost:8080(后端)和http://localhost:5173(前端),看是否有响应。 - 检查控制台错误:打开浏览器开发者工具(F12)的“网络(Network)”标签页,查看API请求是否发出、状态码是什么。常见的
404是URL错了,500是后端服务器内部错误(看后端控制台日志),CORS错误是跨域配置问题。
- 检查服务是否启动:分别访问
CORS问题:
- 症状:浏览器控制台报错包含“CORS policy”、“Access-Control-Allow-Origin”等字样。
- 解决:确认后端
WebConfig中allowedOrigins包含了前端地址(如http://localhost:5173)。注意:如果前端用了代理(如Vite Proxy),则请求不会触发跨域,此时CORS配置可能不生效。
Vite代理配置(开发环境利器): 为了避免CORS和端口问题,可以在Vite中配置代理,让前端开发服务器将所有
/api请求转发到后端。// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 修改请求头中的origin为目标地址 // rewrite: (path) => path.replace(/^\/api/, '') // 如果需要重写路径 } } } })配置后,前端代码中请求
/api/users,Vite会将其代理到http://localhost:8080/api/users。此时,前端请求的baseURL可以设为空或'',因为走的是同源代理。数据格式问题:
- 请求格式:
axios默认发送JSON,如果后端接收@RequestParam,则前端要用params;如果后端是@RequestBody,则前端要用data。 - 响应格式:确认后端返回的数据结构是否与前端解析的匹配。例如,你的Controller直接返回
List<User>,SpringBoot会将其序列化为JSON数组。如果后端统一包装了响应体(如Result<List<User>>),前端就需要从response.data.data里取数据。
- 请求格式:
5.2 项目打包与简易部署
开发完成后,需要将项目构建成可部署的产物。
后端打包:在SpringBoot项目根目录下执行
mvn clean package(Maven)或使用IDEA的Maven插件。会在target目录下生成一个可执行的JAR文件(如demo-0.0.1-SNAPSHOT.jar)。使用java -jar demo-0.0.1-SNAPSHOT.jar即可运行。前端打包:在Vue项目根目录下执行
npm run build。这会在dist目录下生成静态文件(HTML, JS, CSS)。
部署方式:
- 完全分离部署(推荐):将后端JAR包部署到云服务器(如使用
nohup java -jar ... &)。将前端dist目录下的文件部署到Nginx或对象存储(如阿里云OSS、腾讯云COS),并配置Nginx将API请求反向代理到后端服务。这是生产环境的标准做法。 - 静态文件集成部署(简化):将前端打包后的
dist目录下的所有文件,复制到SpringBoot项目的src/main/resources/static目录下。然后打包SpringBoot应用,这样JAR包里就同时包含了前端资源和后端代码。访问http://服务器IP:8080就能看到前端页面。这种方式适合个人演示或极简项目,但失去了前后端独立部署的优势。
5.3 从入门到进阶:下一步可以做什么?
这个项目跑通,只是万里长征第一步。要真正用于生产或深化理解,还有很多方向可以探索:
- 状态管理:当组件间需要共享数据(如用户登录状态)时,引入Pinia或Vuex。
- 路由管理:构建多页面应用,使用Vue Router。
- API安全:为后端API添加认证(JWT)和授权(Spring Security)。
- 数据库进阶:学习更复杂的JPA查询(
@Query注解)、关联映射(@OneToMany)、事务管理(@Transactional)。 - 前端UI库:使用Element Plus、Ant Design Vue等组件库快速搭建美观界面。
- 错误处理与日志:完善前后端的全局错误处理和日志记录。
- 自动化测试:为后端Service层编写单元测试(JUnit),为前端组件编写测试(Vitest)。
- 容器化:使用Docker将前后端分别容器化,用Docker Compose编排,实现环境标准化。
这个入门项目的价值,在于它像一张地图,帮你标记出了前后端分离开发中最核心的几个地标:环境搭建、API定义、数据流动、跨域处理。当你亲手走通一遍之后,再去看那些更复杂的框架和概念,心里就有底了。记住,所有的复杂架构,都是由这些简单的模块组合、演化而来的。先跑通,再优化,逐步深入,这才是最有效的学习路径。