Spring Boot + Vue.js全栈开发最佳实践:从项目初始化到生产环境部署

Bob918
Bob918 2026-02-03T16:16:05+08:00
0 0 1

前言

在现代Web应用开发中,前后端分离架构已经成为主流趋势。Spring Boot作为Java生态中的明星框架,配合Vue.js这一优秀的前端框架,能够构建出高性能、可维护的现代化Web应用。本文将系统性地介绍从项目初始化到生产环境部署的完整开发流程,涵盖后端API设计、前端组件化开发、认证授权、数据库优化等核心要点。

一、项目架构概述

1.1 技术栈选择

本项目采用以下技术栈:

  • 后端:Spring Boot 2.x + Spring Security + JWT + MySQL + MyBatis Plus
  • 前端:Vue.js 3.x + Vue Router + Vuex + Element Plus + Axios
  • 构建工具:Maven + Webpack + Vite
  • 部署环境:Docker + Nginx + Linux服务器

1.2 架构设计原则

采用微服务思想的单体应用架构,遵循以下设计原则:

  • 高内聚低耦合
  • 单一职责原则
  • RESTful API设计规范
  • 前后端完全分离
  • 统一异常处理机制

二、后端开发环境搭建

2.1 Spring Boot项目初始化

使用Spring Initializr创建基础项目:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>backend-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>backend-demo</name>
    
    <properties>
        <java.version>11</java.version>
        <mybatis-plus.version>3.5.2</mybatis-plus.version>
        <jwt.version>0.9.1</jwt.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <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>
        
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>
        
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

2.2 核心配置文件

application.yml

server:
  port: 8080
  
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    
jwt:
  secret: mySecretKeyForTokenGeneration
  expiration: 86400000
  
logging:
  level:
    com.example.demo: debug

三、后端核心功能实现

3.1 JWT认证授权系统

JWT工具类

@Component
public class JwtUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }
    
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    
    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
}

3.2 用户认证控制器

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
                )
            );
            
            final UserDetails userDetails = userService.loadUserByUsername(loginRequest.getUsername());
            final String token = jwtUtil.generateToken(userDetails);
            
            return ResponseEntity.ok(new JwtResponse(token));
        } catch (AuthenticationException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("用户名或密码错误");
        }
    }
    
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
        try {
            userService.register(registerRequest);
            return ResponseEntity.ok("注册成功");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(e.getMessage());
        }
    }
}

3.3 用户服务层

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }
        
        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .authorities("USER")
                .build();
    }
    
    public void register(RegisterRequest request) {
        User existingUser = userMapper.findByUsername(request.getUsername());
        if (existingUser != null) {
            throw new RuntimeException("用户名已存在");
        }
        
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setEmail(request.getEmail());
        user.setCreateTime(new Date());
        
        userMapper.insert(user);
    }
}

四、数据库设计与ORM实现

4.1 数据库表结构设计

-- 用户表
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `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;

-- 角色表
CREATE TABLE `role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '角色名称',
  `description` varchar(200) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户角色关联表
CREATE TABLE `user_role` (
  `user_id` bigint NOT NULL,
  `role_id` bigint NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`),
  KEY `fk_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 MyBatis Plus配置

@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setOverflow(true);
        paginationInnerInterceptor.setMaxLimit(500L);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

4.3 实体类与Mapper

@TableName("user")
@Data
@EqualsAndHashCode(callSuper = false)
public class User extends Model<User> {
    
    private static final long serialVersionUID = 1L;
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    private String username;
    
    private String password;
    
    private String email;
    
    private Date createTime;
    
    private Date updateTime;
}

@Mapper
public interface UserMapper extends BaseMapper<User> {
    
    User findByUsername(String username);
}

五、前端开发环境搭建

5.1 Vue.js项目初始化

使用Vite创建Vue 3项目:

npm create vite@latest frontend-demo --template vue
cd frontend-demo
npm install

package.json依赖

{
  "name": "frontend-demo",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.1.2",
    "element-plus": "^2.2.6",
    "pinia": "^2.0.28",
    "vue": "^3.2.45",
    "vue-router": "^4.1.6"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "vite": "^4.1.0"
  }
}

5.2 项目目录结构

src/
├── assets/              # 静态资源
├── components/          # 公共组件
├── views/               # 页面组件
├── router/              # 路由配置
├── store/               # 状态管理
├── utils/               # 工具函数
├── api/                 # API接口
├── App.vue              # 根组件
└── main.js              # 入口文件

六、前端核心功能实现

6.1 Axios封装与拦截器

// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
  timeout: 5000
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config
  },
  error => {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.error('响应错误:', error)
    ElMessage({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service

6.2 API接口封装

// src/api/auth.js
import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/auth/login',
    method: 'post',
    data
  })
}

export function register(data) {
  return request({
    url: '/auth/register',
    method: 'post',
    data
  })
}

export function getUserInfo() {
  return request({
    url: '/user/info',
    method: 'get'
  })
}

export function logout() {
  return request({
    url: '/auth/logout',
    method: 'post'
  })
}

6.3 路由配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/user'

const routes = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  const token = localStorage.getItem('token')
  
  if (to.meta.requiresAuth && !token) {
    next('/login')
  } else if (to.path === '/login' && token) {
    next('/dashboard')
  } else {
    next()
  }
})

export default router

6.4 Pinia状态管理

// src/store/user.js
import { defineStore } from 'pinia'
import { login, logout, getUserInfo } from '@/api/auth'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    userInfo: null
  }),
  
  actions: {
    async login(loginForm) {
      try {
        const response = await login(loginForm)
        this.token = response.data.token
        localStorage.setItem('token', response.data.token)
        return response
      } catch (error) {
        throw error
      }
    },
    
    async getUserInfo() {
      try {
        const response = await getUserInfo()
        this.userInfo = response.data
        return response
      } catch (error) {
        throw error
      }
    },
    
    logout() {
      this.token = ''
      this.userInfo = null
      localStorage.removeItem('token')
    }
  }
})

七、安全与性能优化

7.1 安全配置

Spring Security配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests(authz -> authz
                    .requestMatchers("/auth/**").permitAll()
                    .anyRequest().authenticated()
                )
                .exceptionHandling(exceptions -> exceptions
                    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                )
                .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                );
        
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

7.2 数据库连接池优化

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000

7.3 缓存优化

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Cacheable(value = "users", key = "#username")
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }
    
    @CacheEvict(value = "users", key = "#user.username")
    public void updateUser(User user) {
        userMapper.updateById(user);
    }
}

八、测试策略

8.1 单元测试

@SpringBootTest
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserMapper userMapper;
    
    @Test
    void testRegisterSuccess() {
        RegisterRequest request = new RegisterRequest();
        request.setUsername("testuser");
        request.setPassword("password");
        request.setEmail("test@example.com");
        
        when(userMapper.findByUsername("testuser")).thenReturn(null);
        
        assertDoesNotThrow(() -> userService.register(request));
    }
    
    @Test
    void testRegisterDuplicateUser() {
        RegisterRequest request = new RegisterRequest();
        request.setUsername("testuser");
        request.setPassword("password");
        request.setEmail("test@example.com");
        
        when(userMapper.findByUsername("testuser")).thenReturn(new User());
        
        assertThrows(RuntimeException.class, () -> userService.register(request));
    }
}

8.2 前端测试

// src/components/LoginForm.spec.js
import { mount } from '@vue/test-utils'
import LoginForm from '@/components/LoginForm.vue'

describe('LoginForm', () => {
  it('renders form fields correctly', () => {
    const wrapper = mount(LoginForm)
    
    expect(wrapper.find('input[name="username"]').exists()).toBe(true)
    expect(wrapper.find('input[name="password"]').exists()).toBe(true)
  })
  
  it('emits login event when form is submitted', async () => {
    const wrapper = mount(LoginForm)
    
    await wrapper.find('input[name="username"]').setValue('testuser')
    await wrapper.find('input[name="password"]').setValue('password')
    
    await wrapper.find('form').trigger('submit.prevent')
    
    expect(wrapper.emitted('login')).toBeTruthy()
  })
})

九、部署方案

9.1 Docker构建

Dockerfile

FROM openjdk:11-jre-slim

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

docker-compose.yml

version: '3.8'

services:
  backend:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    depends_on:
      - mysql
    networks:
      - app-network

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: demo_db
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network

  frontend:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./dist:/usr/share/nginx/html
    depends_on:
      - backend
    networks:
      - app-network

volumes:
  mysql_data:

networks:
  app-network:
    driver: bridge

9.2 Nginx配置

server {
    listen 80;
    server_name localhost;
    
    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }
}

9.3 生产环境配置

application-prod.yml

spring:
  datasource:
    url: jdbc:mysql://mysql:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: rootpassword
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 15
      minimum-idle: 3
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
logging:
  level:
    com.example.demo: info
    org.springframework.web: info
    
server:
  port: 8080
  
jwt:
  secret: ${JWT_SECRET:prodSecretKey}
  expiration: 86400000

十、最佳实践总结

10.1 开发规范

  1. 代码风格统一:使用Lombok简化POJO,遵循阿里巴巴Java开发手册
  2. API设计规范:采用RESTful风格,统一响应格式
  3. 异常处理机制:全局异常处理器统一处理业务异常和系统异常
  4. 日志管理:合理使用日志级别,便于问题排查

10.2 性能优化要点

  1. 数据库优化:合理设计索引,避免N+1查询问题
  2. 缓存策略:适当使用Redis缓存热点数据
  3. 连接池配置:根据实际负载调整连接池参数
  4. 异步处理:对于耗时操作使用异步处理机制

10.3 安全防护措施

  1. 认证授权:JWT token机制,避免session依赖
  2. 输入验证:后端严格校验所有输入参数
  3. CORS配置:合理配置跨域资源共享策略
  4. 密码安全:使用BCrypt加密存储用户密码

10.4 部署运维建议

  1. 容器化部署:Docker化部署提高环境一致性
  2. 监控告警:集成Prometheus + Grafana进行系统监控
  3. 日志管理:使用ELK进行日志收集和分析
  4. 备份策略:定期备份数据库和重要配置文件

结语

本文详细介绍了Spring Boot + Vue.js全栈开发的完整流程,从项目初始化到生产环境部署,涵盖了前后端分离架构的核心技术要点。通过实际的代码示例和最佳实践,为开发者提供了可直接参考的开发方案。

在实际项目中,还需要根据具体业务需求进行相应的调整和优化。建议团队在开发过程中建立完善的测试体系,注重代码质量,持续优化系统性能,确保应用的稳定性和可维护性。随着技术的不断发展,我们也要保持学习的态度,及时跟进新技术和新框架,不断提升开发效率和产品质量。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000