前言
在当今快速发展的Web开发领域,全栈开发已成为技术人员的必备技能。本文将深入探讨如何使用Spring Boot、Vue.js和MySQL构建现代化的Web应用程序,通过实际项目案例,为你提供一套完整的实施技巧和最佳实践。
本文能帮你解决什么? 快速搭建一个具备用户管理、权限控制、前后端分离的生产级Web应用
你将学到什么? Spring Boot后端API开发、Vue 3前端工程化、JWT认证、Docker容器化部署
适合谁? 有一定Java/JavaScript基础,希望系统掌握全栈开发的中级开发者
前置知识:Java基础、Spring基础、Vue基础、MySQL基础
技术栈概览
后端架构
-
Spring Boot 2.7.x:简化Spring应用开发
-
Spring Security:安全认证框架
-
MyBatis Plus:ORM框架增强
-
JWT:无状态认证机制
-
Redis:缓存解决方案
前端架构
-
Vue 3.x:渐进式JavaScript框架
-
Vue Router:路由管理
-
Vuex:状态管理
-
Element Plus:UI组件库
-
Axios:HTTP客户端
数据存储
-
MySQL 8.0:关系型数据库
-
Redis:内存数据结构存储
🎯 开篇导读(图文引导)
“一张图胜过千言万语” —— 我们先用一张系统架构图,带你一眼看懂整个技术栈如何协同工作。
📊 系统架构图(原创设计)
plaintext
┌──────────────────────────────────────────────────────────────┐
│ 前端(Vue3 + ElementPlus) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 登录页 │ │ 用户列表页 │ │ 权限管理页 │ │
│ │ Login.vue │ │ User.vue │ │ Role.vue │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┴────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────┘
│ Axios
┌──────────────────────────────┼───────────────────────────────┐
│ │ │
│ API网关(Nginx + HTTPS + Gzip) │
│ │ │
└──────────────────────────────┼───────────────────────────────┘
│
┌──────────────────────────────┼───────────────────────────────┐
│ │ Spring Boot 2.7.x │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Controller │ │ Service │ │ Mapper (MyBatis) │ │
│ │ 用户接口 │ │ 业务逻辑 │ │ 数据访问 │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┴────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────┘
│
┌──────────────────────────────┼───────────────────────────────┐
│ │ 数据层 │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ MySQL 8.0 │ │ Redis │ │ 文件存储(OSS) │ │
│ │ 主从复制 │ │ 缓存/会话 │ │ 头像/日志 │ │
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────────┘
✅ 图解说明:
-
前端采用 Vue3 + ElementPlus,组件化开发,支持响应式布局
-
后端使用 Spring Boot + MyBatis Plus,RESTful 接口设计
-
数据层支持 主从复制 + Redis缓存 + OSS对象存储
-
全链路支持 HTTPS、Gzip压缩、JWT认证、权限控制
项目架构设计
系统架构图
┌─────────────────────────────────────────────────────────┐
│ 前端层 (Vue.js) │
├─────────────────────────────────────────────────────────┤
│ API网关 (Nginx) │
├─────────────────────────────────────────────────────────┤
│ 应用层 (Spring Boot) │
│ ┌─────────────┬──────────────┬─────────────────────┐ │
│ │ 控制器 │ 业务逻辑 │ 数据访问 │ │
│ │ Controller │ Service │ Mapper │ │
│ └─────────────┴──────────────┴─────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 数据层 (MySQL + Redis) │
└─────────────────────────────────────────────────────────┘
数据库设计示例
sql
-- 用户表
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`status` tinyint DEFAULT '1' COMMENT '状态:1-正常,0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 角色表
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
后端开发实战
项目创建:
# 使用Spring Initializr创建项目
# 访问 https://start.spring.io/
选择: # - Spring Boot: 2.7.x
# - Packaging: Jar
# - Java: 8/11/17
# - Dependencies: Web, Security, MyBatis, MySQL, Redis
1. Spring Boot项目初始化
xml
<!-- pom.xml 核心依赖 -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
关键点说明:
-
MyBatis Plus 3.x版本对MyBatis进行了深度封装,可减少80%的SQL编写
-
MySQL 8.0驱动类名为
com.mysql.cj.jdbc.Driver,与5.x不同 -
JWT版本选择0.9.1,这是稳定且广泛使用的版本
2. 配置文件优化
yaml
# application.yml
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fullstack_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
redis:
host: localhost
port: 6379
password:
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
logging:
level:
com.example: debug
🔧 配置要点:
-
时区问题:MySQL 8.0必须设置
serverTimezone=Asia/Shanghai,否则时间会差8小时 -
SSL警告:本地开发可设置
useSSL=false,生产环境必须使用SSL -
连接泄漏防护:HikariCP是Spring Boot 2.x默认连接池,性能优于DBCP2
3. 统一响应封装
java
// Result.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
为什么需要统一响应格式?
-
前端可以统一处理响应逻辑
-
便于日志记录和问题排查
-
标准化接口文档
4. JWT认证实现
java
// JwtTokenProvider.java
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("authorities", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration * 1000))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
log.error("JWT token validation error: {}", e.getMessage());
return false;
}
}
}
5. 用户管理功能实现
java
// UserController.java
@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody LoginDTO loginDTO) {
try {
Map<String, Object> result = userService.login(loginDTO);
return Result.success(result);
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
@GetMapping("/page")
@PreAuthorize("hasRole('ADMIN')")
public Result<PageResult<UserVO>> getUserPage(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String username) {
Page<UserVO> page = new Page<>(current, size);
IPage<UserVO> userPage = userService.getUserPage(page, username);
PageResult<UserVO> pageResult = new PageResult<>();
pageResult.setRecords(userPage.getRecords());
pageResult.setTotal(userPage.getTotal());
pageResult.setCurrent(current);
pageResult.setSize(size);
return Result.success(pageResult);
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public Result<String> addUser(@RequestBody @Valid UserDTO userDTO) {
userService.addUser(userDTO);
return Result.success("用户添加成功");
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result<String> updateUser(@PathVariable Long id, @RequestBody @Valid UserDTO userDTO) {
userService.updateUser(id, userDTO);
return Result.success("用户更新成功");
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result<String> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success("用户删除成功");
}
}
6. 全局异常处理
java
// GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error("系统内部错误");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String errorMessage = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(errorMessage);
}
@ExceptionHandler(BusinessException.class)
public Result<String> handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
}
前端开发实战
1. Vue项目初始化
bash
# 使用Vite创建Vue3项目
npm create vite@latest fullstack-frontend --template vue
# 安装依赖
cd fullstack-frontend
npm install
# 安装UI组件库和工具
npm install element-plus axios vue-router pinia @element-plus/icons-vue
# 安装开发工具
npm install -D sass vite-plugin-compression
2. 项目结构组织
src/
├── api/ # API接口管理
├── assets/ # 静态资源
├── components/ # 公共组件
├── directives/ # 自定义指令
├── layout/ # 布局组件
├── router/ # 路由配置
├── store/ # 状态管理
├── styles/ # 全局样式
├── utils/ # 工具函数
├── views/ # 页面组件
└── main.js # 入口文件
3. Axios请求封装
JavaScript
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/user'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
config => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
const { code, message, data } = response.data
if (code === 200) {
return data
} else {
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message || '请求失败'))
}
},
error => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
router.push('/login')
}
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
)
export default request
4. Pinia状态管理
JavaScript
// store/user.js
import { defineStore } from 'pinia'
import { login, getUserInfo } from '@/api/user'
import router from '@/router'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null,
roles: []
}),
getters: {
isLoggedIn: (state) => !!state.token,
hasRole: (state) => (role) => state.roles.includes(role)
},
actions: {
async login(loginForm) {
try {
const response = await login(loginForm)
this.token = response.token
localStorage.setItem('token', this.token)
await this.getUserInfo()
router.push('/')
} catch (error) {
throw error
}
},
async getUserInfo() {
try {
const data = await getUserInfo()
this.userInfo = data.userInfo
this.roles = data.roles
} catch (error) {
this.logout()
throw error
}
},
logout() {
this.token = ''
this.userInfo = null
this.roles = []
localStorage.removeItem('token')
router.push('/login')
}
}
})
5. 用户管理页面实现
vue
<!-- views/user/index.vue -->
<template>
<div class="user-container">
<el-card>
<template #header>
<div class="card-header">
<span>用户管理</span>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增用户
</el-button>
</div>
</template>
<!-- 搜索表单 -->
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="用户名">
<el-input v-model="searchForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 用户表格 -->
<el-table :data="userList" v-loading="loading" border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pageParams.current"
v-model:page-size="pageParams.size"
:total="total"
@current-change="getUserList"
@size-change="getUserList"
layout="total, sizes, prev, pager, next, jumper"
/>
</el-card>
<!-- 用户表单对话框 -->
<UserFormDialog
v-model="dialogVisible"
:user-id="currentUserId"
@success="getUserList"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserPage, deleteUser } from '@/api/user'
import UserFormDialog from './components/UserFormDialog.vue'
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const currentUserId = ref(null)
const searchForm = reactive({
username: ''
})
const pageParams = reactive({
current: 1,
size: 10
})
const getUserList = async () => {
loading.value = true
try {
const params = {
...pageParams,
...searchForm
}
const data = await getUserPage(params)
userList.value = data.records
total.value = data.total
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
pageParams.current = 1
getUserList()
}
const handleReset = () => {
searchForm.username = ''
handleSearch()
}
const handleAdd = () => {
currentUserId.value = null
dialogVisible.value = true
}
const handleEdit = (row) => {
currentUserId.value = row.id
dialogVisible.value = true
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
type: 'warning'
})
await deleteUser(row.id)
ElMessage.success('删除成功')
getUserList()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
}
}
}
onMounted(() => {
getUserList()
})
</script>
<style scoped lang="scss">
.user-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.el-pagination {
margin-top: 20px;
justify-content: flex-end;
}
</style>
6. 用户表单组件
vue
<!-- views/user/components/UserFormDialog.vue -->
<template>
<el-dialog
v-model="visible"
:title="userId ? '编辑用户' : '新增用户'"
width="500px"
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { addUser, updateUser, getUserDetail } from '@/api/user'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
userId: {
type: [String, Number],
default: null
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref(null)
const submitLoading = ref(false)
const form = reactive({
username: '',
email: '',
phone: '',
status: 1
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
const getUserDetail = async () => {
if (!props.userId) return
try {
const data = await getUserDetail(props.userId)
Object.assign(form, data)
} catch (error) {
console.error('获取用户详情失败:', error)
}
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
if (props.userId) {
await updateUser(props.userId, form)
ElMessage.success('更新成功')
} else {
await addUser(form)
ElMessage.success('添加成功')
}
emit('success')
handleClose()
} catch (error) {
console.error('提交失败:', error)
} finally {
submitLoading.value = false
}
}
const handleClose = () => {
formRef.value?.resetFields()
Object.assign(form, {
username: '',
email: '',
phone: '',
status: 1
})
emit('update:modelValue', false)
}
watch(() => props.modelValue, (val) => {
visible.value = val
if (val && props.userId) {
getUserDetail()
}
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
</script>
部署与优化
1. 生产环境配置
yaml
# application-prod.yml
server:
port: 8080
tomcat:
threads:
max: 200
min-spare: 10
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 20000
redis:
cluster:
nodes:
- redis://127.0.0.1:7001
- redis://127.0.0.1:7002
- redis://127.0.0.1:7003
max-redirects: 3
# 日志配置
logging:
level:
root: INFO
com.example: INFO
file:
name: logs/application.log
max-size: 100MB
max-history: 30
2. 前端打包优化
JavaScript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
vue(),
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-vendor': ['vue', 'vue-router', 'pinia']
}
}
},
chunkSizeWarningLimit: 1000
}
})
3. Docker部署配置
dockerfile
# Dockerfile
FROM openjdk:8-jre-alpine
VOLUME /tmp
COPY target/fullstack-backend.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
yaml
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: fullstack-mysql
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: fullstack_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: fullstack-redis
ports:
- "6379:6379"
backend:
build: ./backend
container_name: fullstack-backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
frontend:
image: nginx:alpine
container_name: fullstack-frontend
ports:
- "80:80"
volumes:
- ./frontend/dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf
volumes:
mysql_data:
性能优化技巧
1. 数据库优化
sql
-- 添加索引
CREATE INDEX idx_username ON sys_user(username);
CREATE INDEX idx_create_time ON sys_user(create_time);
-- 分页查询优化
SELECT * FROM sys_user
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{pageSize};
2. Redis缓存策略
java
// RedisConfig.java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer序列化
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
3. 前端性能优化
JavaScript
// 路由懒加载
const routes = [
{
path: '/user',
component: () => import('@/views/user/index.vue'),
meta: { title: '用户管理' }
}
]
// 虚拟滚动
<template>
<el-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
安全最佳实践
1. 密码加密
java
// PasswordEncoderConfig.java
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度12
}
}
// 使用示例
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public void addUser(UserDTO userDTO) {
User user = new User();
user.setUsername(userDTO.getUsername());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userMapper.insert(user);
}
}
2. XSS防护
java
// XssFilter.java
@Component
@WebFilter(urlPatterns = "/*")
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
XssHttpServletRequestWrapper xssRequest =
new XssHttpServletRequestWrapper(httpRequest);
chain.doFilter(xssRequest, response);
}
}
3. SQL注入防护
java
// 使用MyBatis Plus防止SQL注入
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 安全的查询方式
@Select("SELECT * FROM sys_user WHERE username = #{username}")
User findByUsername(@Param("username") String username);
}
总结
本文详细介绍了Spring Boot + Vue + MySQL全栈开发的完整实施过程,从项目架构设计到具体代码实现,再到部署优化和安全防护。通过这套技术栈,我们可以构建出高性能、可维护的现代化Web应用程序。
感谢你能读到这里。
如果这篇博客刚好解决了你的痛点,请点个【👍点赞】+【⭐收藏】,让它有机会帮到更多小伙伴;
如果还有疑问,评论区见,我必回复;
也欢迎你【📬私信】晒出你的运行截图,我们一起把项目打磨得更丝滑。
愿你我都能写出优雅代码,不负时光,不负自己。
关键技术点回顾:
-
后端架构:采用Spring Boot快速开发,结合Spring Security实现安全认证,使用MyBatis Plus简化数据访问
-
前端架构:基于Vue 3的组件化开发,配合Element Plus提供优秀的用户体验
-
性能优化:通过Redis缓存、数据库索引、前端懒加载等手段提升系统性能
-
安全防护:JWT认证、密码加密、XSS防护、SQL注入防护等多重安全措施
后续学习建议:
-
深入学习微服务架构,将单体应用拆分为微服务
-
探索容器化部署,使用Kubernetes进行容器编排
-
学习前端进阶技术,如TypeScript、Vue 3 Composition API等
-
关注云原生技术,如Service Mesh、Serverless等新兴技术

被折叠的 条评论
为什么被折叠?



