引言
在当今快速发展的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
性能优化建议
- 数据库连接池配置:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
- 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)