Spring Boot 3.0 + React + Docker 构建现代化全栈应用:从零到生产部署全流程

GladAlice
GladAlice 2026-01-25T16:14:16+08:00
0 0 2

引言

在当今快速发展的Web开发领域,构建现代化的全栈应用已成为开发者的核心技能之一。Spring Boot 3.0作为Spring生态的最新版本,为后端开发带来了诸多新特性;React作为流行的前端框架,提供了优秀的用户体验;而Docker则为应用的容器化部署提供了完美的解决方案。本文将带领读者从零开始,构建一个完整的现代化全栈Web应用,并完成生产环境的部署。

技术栈概览

在开始之前,让我们先了解一下我们将使用的技术栈:

  • Spring Boot 3.0:基于Java 17的现代后端框架,提供自动配置、简化开发等特性
  • React 18:用于构建用户界面的JavaScript库,支持组件化开发
  • Docker:容器化平台,确保应用在不同环境中的一致性
  • MySQL:关系型数据库,用于数据持久化
  • RESTful API:前后端通信的标准协议

项目初始化与架构设计

项目结构规划

首先,我们需要规划整个项目的目录结构。典型的现代化全栈项目通常采用前后端分离的架构:

modern-fullstack-app/
├── backend/                 # Spring Boot 后端项目
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/app/
│   │   │   │   ├── controller/    # 控制器层
│   │   │   │   ├── service/       # 服务层
│   │   │   │   ├── repository/    # 数据访问层
│   │   │   │   ├── model/         # 实体类
│   │   │   │   └── config/        # 配置类
│   │   │   └── resources/
│   │   │       ├── application.yml
│   │   │       └── static/
│   │   └── test/
│   └── pom.xml              # Maven配置文件
├── frontend/                # React 前端项目
│   ├── public/
│   ├── src/
│   │   ├── components/        # 组件目录
│   │   ├── pages/             # 页面组件
│   │   ├── services/          # API服务
│   │   ├── hooks/             # 自定义Hook
│   │   └── App.js
│   └── package.json         # npm配置文件
├── docker-compose.yml       # Docker编排文件
└── README.md

后端项目初始化

使用Spring Initializr创建Spring Boot 3.0项目,选择以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

后端开发:Spring Boot 3.0 实现

数据模型设计

让我们首先创建一个简单的用户实体类:

package com.example.app.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Column(unique = true, nullable = false)
    private String username;
    
    @Email(message = "邮箱格式不正确")
    @NotBlank(message = "邮箱不能为空")
    @Column(unique = true, nullable = false)
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Column(nullable = false)
    private String password;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // 构造函数
    public User() {}
    
    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
    
    // Getter和Setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

数据访问层实现

创建用户Repository接口:

package com.example.app.repository;

import com.example.app.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
    Boolean existsByUsername(String username);
    Boolean existsByEmail(String email);
}

服务层实现

创建UserService来处理业务逻辑:

package com.example.app.service;

import com.example.app.model.User;
import com.example.app.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }
    
    public User createUser(User user) {
        return userRepository.save(user);
    }
    
    public User updateUser(Long id, User userDetails) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("用户不存在"));
        
        user.setUsername(userDetails.getUsername());
        user.setEmail(userDetails.getEmail());
        user.setPassword(userDetails.getPassword());
        
        return userRepository.save(user);
    }
    
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
    
    public Optional<User> getUserByUsername(String username) {
        return userRepository.findByUsername(username);
    }
    
    public Boolean existsByUsername(String username) {
        return userRepository.existsByUsername(username);
    }
    
    public Boolean existsByEmail(String email) {
        return userRepository.existsByEmail(email);
    }
}

控制器层实现

创建UserController来处理HTTP请求:

package com.example.app.controller;

import com.example.app.model.User;
import com.example.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return new ResponseEntity<>(users, HttpStatus.OK);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                  .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody User user) {
        if (userService.existsByUsername(user.getUsername())) {
            return ResponseEntity.badRequest()
                    .body("用户名已存在");
        }
        
        if (userService.existsByEmail(user.getEmail())) {
            return ResponseEntity.badRequest()
                    .body("邮箱已被使用");
        }
        
        User savedUser = userService.createUser(user);
        return new ResponseEntity<>(savedUser, HttpStatus.CREATED);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<?> updateUser(@PathVariable Long id, 
                                       @Valid @RequestBody User userDetails) {
        Optional<User> existingUser = userService.getUserById(id);
        
        if (existingUser.isEmpty()) {
            return ResponseEntity.notFound().build();
        }
        
        // 检查用户名是否被其他用户使用
        Optional<User> userByUsername = userService.getUserByUsername(userDetails.getUsername());
        if (userByUsername.isPresent() && !userByUsername.get().getId().equals(id)) {
            return ResponseEntity.badRequest()
                    .body("用户名已存在");
        }
        
        // 检查邮箱是否被其他用户使用
        Optional<User> userByEmail = userService.getUserByUsername(userDetails.getEmail());
        if (userByEmail.isPresent() && !userByEmail.get().getId().equals(id)) {
            return ResponseEntity.badRequest()
                    .body("邮箱已被使用");
        }
        
        User updatedUser = userService.updateUser(id, userDetails);
        return ResponseEntity.ok(updatedUser);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteUser(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        
        if (user.isEmpty()) {
            return ResponseEntity.notFound().build();
        }
        
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

配置文件设置

application.yml中配置数据库连接和服务器信息:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/modern_app?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: true
    
  application:
    name: modern-fullstack-app

logging:
  level:
    com.example.app: DEBUG

前端开发:React 实现

项目初始化

使用Create React App创建前端项目:

npx create-react-app frontend
cd frontend
npm install axios react-router-dom

API服务封装

创建一个API服务文件来处理HTTP请求:

// src/services/api.js
import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api/users';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 可以在这里添加认证token等
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response?.status === 401) {
      // 处理未授权错误
      console.error('未授权访问');
    }
    return Promise.reject(error);
  }
);

export const userApi = {
  getAllUsers: () => api.get('/'),
  getUserById: (id) => api.get(`/${id}`),
  createUser: (userData) => api.post('/', userData),
  updateUser: (id, userData) => api.put(`/${id}`, userData),
  deleteUser: (id) => api.delete(`/${id}`),
};

export default api;

用户组件实现

创建用户列表组件:

// src/components/UserList.js
import React, { useState, useEffect } from 'react';
import { userApi } from '../services/api';

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await userApi.getAllUsers();
      setUsers(response.data);
      setError(null);
    } catch (err) {
      setError('获取用户列表失败');
      console.error('Error fetching users:', err);
    } finally {
      setLoading(false);
    }
  };

  const handleDeleteUser = async (id) => {
    if (window.confirm('确定要删除这个用户吗?')) {
      try {
        await userApi.deleteUser(id);
        // 重新获取用户列表
        fetchUsers();
      } catch (err) {
        setError('删除用户失败');
        console.error('Error deleting user:', err);
      }
    }
  };

  if (loading) return <div className="loading">加载中...</div>;
  if (error) return <div className="error">{error}</div>;

  return (
    <div className="user-list">
      <h2>用户列表</h2>
      <table className="users-table">
        <thead>
          <tr>
            <th>ID</th>
            <th>用户名</th>
            <th>邮箱</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.username}</td>
              <td>{user.email}</td>
              <td>
                <button 
                  onClick={() => handleDeleteUser(user.id)}
                  className="delete-btn"
                >
                  删除
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default UserList;

用户表单组件

创建用户添加/编辑表单:

// src/components/UserForm.js
import React, { useState, useEffect } from 'react';
import { userApi } from '../services/api';

const UserForm = ({ userId, onUserSaved, onCancel }) => {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});
  const [loading, setLoading] = useState(false);
  const [isEditing, setIsEditing] = useState(false);

  useEffect(() => {
    if (userId) {
      fetchUser(userId);
      setIsEditing(true);
    }
  }, [userId]);

  const fetchUser = async (id) => {
    try {
      const response = await userApi.getUserById(id);
      setFormData(response.data);
    } catch (err) {
      console.error('获取用户信息失败:', err);
    }
  };

  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.username.trim()) {
      newErrors.username = '用户名不能为空';
    }
    
    if (!formData.email.trim()) {
      newErrors.email = '邮箱不能为空';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '邮箱格式不正确';
    }
    
    if (!formData.password.trim()) {
      newErrors.password = '密码不能为空';
    } else if (formData.password.length < 6) {
      newErrors.password = '密码至少需要6个字符';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    // 清除对应的错误信息
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }

    setLoading(true);
    
    try {
      if (isEditing) {
        await userApi.updateUser(userId, formData);
      } else {
        await userApi.createUser(formData);
      }
      
      onUserSaved();
    } catch (err) {
      console.error('保存用户失败:', err);
      alert('保存失败,请重试');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="user-form">
      <h2>{isEditing ? '编辑用户' : '添加用户'}</h2>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>用户名:</label>
          <input
            type="text"
            name="username"
            value={formData.username}
            onChange={handleChange}
            className={errors.username ? 'error' : ''}
          />
          {errors.username && <span className="error-message">{errors.username}</span>}
        </div>
        
        <div className="form-group">
          <label>邮箱:</label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            className={errors.email ? 'error' : ''}
          />
          {errors.email && <span className="error-message">{errors.email}</span>}
        </div>
        
        <div className="form-group">
          <label>密码:</label>
          <input
            type="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            className={errors.password ? 'error' : ''}
          />
          {errors.password && <span className="error-message">{errors.password}</span>}
        </div>
        
        <div className="form-actions">
          <button type="submit" disabled={loading}>
            {loading ? '保存中...' : isEditing ? '更新用户' : '添加用户'}
          </button>
          {onCancel && (
            <button type="button" onClick={onCancel} className="cancel-btn">
              取消
            </button>
          )}
        </div>
      </form>
    </div>
  );
};

export default UserForm;

主应用组件

整合所有组件的主应用组件:

// src/App.js
import React, { useState } from 'react';
import UserList from './components/UserList';
import UserForm from './components/UserForm';
import './App.css';

function App() {
  const [showForm, setShowForm] = useState(false);
  const [editingUserId, setEditingUserId] = useState(null);

  const handleUserSaved = () => {
    setShowForm(false);
    setEditingUserId(null);
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1>现代化全栈应用</h1>
      </header>
      
      <main className="main-content">
        {showForm ? (
          <UserForm 
            userId={editingUserId}
            onUserSaved={handleUserSaved}
            onCancel={() => {
              setShowForm(false);
              setEditingUserId(null);
            }}
          />
        ) : (
          <>
            <div className="actions">
              <button 
                onClick={() => setShowForm(true)}
                className="add-user-btn"
              >
                添加用户
              </button>
            </div>
            <UserList />
          </>
        )}
      </main>
    </div>
  );
}

export default App;

Docker容器化部署

Dockerfile配置

为Spring Boot后端创建Dockerfile:

# backend/Dockerfile
FROM openjdk:17-jdk-slim

# 设置工作目录
WORKDIR /app

# 复制jar文件
COPY target/*.jar app.jar

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

为React前端创建Dockerfile:

# frontend/Dockerfile
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 使用nginx serve静态文件
FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Docker Compose配置

创建docker-compose.yml文件来编排整个应用:

version: '3.8'

services:
  # 数据库服务
  database:
    image: mysql:8.0
    container_name: mysql-db
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: modern_app
      MYSQL_USER: app_user
      MYSQL_PASSWORD: app_password
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-network

  # 后端服务
  backend:
    build: 
      context: ./backend
      dockerfile: Dockerfile
    container_name: spring-boot-app
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://database:3306/modern_app?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
      SPRING_DATASOURCE_USERNAME: app_user
      SPRING_DATASOURCE_PASSWORD: app_password
    depends_on:
      - database
    networks:
      - app-network

  # 前端服务
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: react-app
    ports:
      - "3000:80"
    depends_on:
      - backend
    networks:
      - app-network

volumes:
  db_data:

networks:
  app-network:
    driver: bridge

初始化数据库脚本

创建init.sql文件来初始化数据库:

-- init.sql
CREATE DATABASE IF NOT EXISTS modern_app;
USE modern_app;

CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 插入测试数据
INSERT INTO users (username, email, password) VALUES 
('admin', 'admin@example.com', 'password123'),
('user1', 'user1@example.com', 'password123'),
('user2', 'user2@example.com', 'password123');

环境配置与优化

开发环境配置

在开发环境中,我们需要配置不同的属性文件:

# application-dev.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/modern_app_dev?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: password
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect

server:
  port: 8080

logging:
  level:
    com.example.app: DEBUG

生产环境配置

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://database:3306/modern_app?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: false
        hbm2ddl:
          auto: validate

server:
  port: ${PORT:8080}

logging:
  level:
    com.example.app: INFO

性能优化建议

  1. 数据库连接池配置
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
  1. API缓存优化
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Cacheable(value = "users", key = "#id")
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                  .orElse(ResponseEntity.notFound().build());
    }
}

部署与监控

CI/CD流水线配置

创建.github/workflows/deploy.yml

name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up JDK 17
      uses: actions/setup-java@v2
      with:
        java-version: '17'
        distribution: 'adopt'
    
    - name: Build backend
      run: |
        cd backend
        mvn clean package -DskipTests
    
    - name: Build frontend
      run: |
        cd frontend
        npm install
        npm run build
    
    - name: Deploy to server
      run: |
        # 部署逻辑
        echo "Deploying application..."

健康检查配置

在Spring Boot应用中添加健康检查端点:

package com.example.app.config;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        // 实现数据库连接检查逻辑
        try {
            // 这里可以添加实际的数据库连接检查
            return Health.up().withDetail("database", "Database is running").build();
        } catch (Exception e) {
            return Health.down().withDetail("database", "Database is down").build();
        }
    }
}

安全最佳实践

跨域配置优化

@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000