引言
随着现代Web应用需求的不断增长,全栈开发技能已成为开发者必备的核心能力。Spring Boot 3.0作为Spring生态的最新版本,带来了Java 17的支持和诸多性能优化;而React 18则通过并发渲染、自动批处理等新特性,为前端开发提供了更强大的工具。本文将带你从零开始,使用这两个技术栈构建一个现代化的企业级全栈应用。
技术选型与环境准备
Spring Boot 3.0 特性概览
Spring Boot 3.0基于Java 17,引入了以下重要特性:
- 支持Java 17的新特性
- 改进的WebFlux响应式编程支持
- 性能优化和内存使用改进
- 更好的Micrometer监控集成
React 18 核心更新
React 18的主要改进包括:
- 自动批处理(Automatic Batching)
- 新的并发渲染API
- 改进的Suspense支持
- 新的useId、useSyncExternalStore等Hook
开发环境搭建
# 检查Java版本
java -version
# 应该显示Java 17或更高版本
# 安装Node.js (建议16.x或18.x)
node --version
npm --version
# 创建Spring Boot项目
# 使用Spring Initializr或IDE插件
# 选择依赖:Web, JPA, H2, Lombok, Validation
项目初始化与架构设计
Spring Boot项目结构
my-fullstack-app/
├── backend/ # 后端代码
│ ├── src/main/java/com/example/
│ │ └── demo/
│ │ ├── DemoApplication.java
│ │ ├── config/
│ │ ├── controller/
│ │ ├── model/
│ │ ├── repository/
│ │ ├── service/
│ │ └── dto/
│ └── src/main/resources/
├── frontend/ # 前端代码
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ ├── hooks/
│ │ ├── context/
│ │ └── App.jsx
│ └── package.json
└── README.md
项目配置文件
application.yml (Spring Boot后端)
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
logging:
level:
com.example.demo: DEBUG
RESTful API设计与实现
用户实体模型
// User.java
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
@Column(nullable = false)
private String password;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
用户数据传输对象
// UserDTO.java
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDTO {
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
用户服务层实现
// UserService.java
package com.example.demo.service;
import com.example.demo.dto.UserDTO;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserRepository userRepository;
public List<UserDTO> getAllUsers() {
return userRepository.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
public Optional<UserDTO> getUserById(Long id) {
return userRepository.findById(id).map(this::toDTO);
}
public UserDTO createUser(UserDTO userDTO) {
User user = toEntity(userDTO);
User savedUser = userRepository.save(user);
return toDTO(savedUser);
}
public Optional<UserDTO> updateUser(Long id, UserDTO userDTO) {
return userRepository.findById(id)
.map(user -> {
user.setUsername(userDTO.getUsername());
user.setEmail(userDTO.getEmail());
User updatedUser = userRepository.save(user);
return toDTO(updatedUser);
});
}
public boolean deleteUser(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
private UserDTO toDTO(User user) {
return UserDTO.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}
private User toEntity(UserDTO userDTO) {
return User.builder()
.username(userDTO.getUsername())
.email(userDTO.getEmail())
.password(userDTO.getPassword()) // 实际项目中应该加密
.build();
}
}
用户控制器
// UserController.java
package com.example.demo.controller;
import com.example.demo.dto.UserDTO;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
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")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
Optional<UserDTO> user = userService.getUserById(id);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
try {
UserDTO createdUser = userService.createUser(userDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserDTO userDTO) {
Optional<UserDTO> updatedUser = userService.updateUser(id, userDTO);
return updatedUser.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
boolean deleted = userService.deleteUser(id);
return deleted ? ResponseEntity.noContent().build()
: ResponseEntity.notFound().build();
}
}
React 18 前端开发实践
项目初始化与依赖安装
# 创建React应用
npx create-react-app frontend --template typescript
# 安装必要的依赖
cd frontend
npm install axios react-router-dom @mui/material @emotion/react @emotion/styled
API服务封装
// src/services/api.js
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8080/api';
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token等
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
if (error.response?.status === 401) {
// 处理未授权错误
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
// src/services/userService.js
import api from './api';
export const userService = {
getUsers: async () => {
try {
const response = await api.get('/users');
return response;
} catch (error) {
throw new Error(`获取用户列表失败: ${error.message}`);
}
},
getUserById: async (id) => {
try {
const response = await api.get(`/users/${id}`);
return response;
} catch (error) {
throw new Error(`获取用户信息失败: ${error.message}`);
}
},
createUser: async (userData) => {
try {
const response = await api.post('/users', userData);
return response;
} catch (error) {
throw new Error(`创建用户失败: ${error.message}`);
}
},
updateUser: async (id, userData) => {
try {
const response = await api.put(`/users/${id}`, userData);
return response;
} catch (error) {
throw new Error(`更新用户失败: ${error.message}`);
}
},
deleteUser: async (id) => {
try {
const response = await api.delete(`/users/${id}`);
return response;
} catch (error) {
throw new Error(`删除用户失败: ${error.message}`);
}
},
};
用户管理组件
// src/components/UserList.jsx
import React, { useState, useEffect } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Box,
Typography,
CircularProgress,
Alert
} from '@mui/material';
import { userService } from '../services/userService';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [openDialog, setOpenDialog] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const data = await userService.getUsers();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleCreateUser = async (e) => {
e.preventDefault();
try {
await userService.createUser(formData);
setOpenDialog(false);
setFormData({ username: '', email: '', password: '' });
fetchUsers(); // 刷新列表
} catch (err) {
setError(err.message);
}
};
const handleEditUser = (user) => {
setEditingUser(user);
setFormData({
username: user.username,
email: user.email,
password: ''
});
setOpenDialog(true);
};
const handleUpdateUser = async (e) => {
e.preventDefault();
try {
await userService.updateUser(editingUser.id, formData);
setOpenDialog(false);
setEditingUser(null);
fetchUsers(); // 刷新列表
} catch (err) {
setError(err.message);
}
};
const handleDeleteUser = async (id) => {
if (window.confirm('确定要删除这个用户吗?')) {
try {
await userService.deleteUser(id);
fetchUsers(); // 刷新列表
} catch (err) {
setError(err.message);
}
}
};
const handleCloseDialog = () => {
setOpenDialog(false);
setEditingUser(null);
setFormData({ username: '', email: '', password: '' });
};
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
);
}
return (
<Box sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h5">用户管理</Typography>
<Button
variant="contained"
onClick={() => setOpenDialog(true)}
>
添加用户
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>用户名</TableCell>
<TableCell>邮箱</TableCell>
<TableCell>创建时间</TableCell>
<TableCell>操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{new Date(user.createdAt).toLocaleString()}</TableCell>
<TableCell>
<Button
size="small"
onClick={() => handleEditUser(user)}
sx={{ mr: 1 }}
>
编辑
</Button>
<Button
size="small"
color="error"
onClick={() => handleDeleteUser(user.id)}
>
删除
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={openDialog} onClose={handleCloseDialog}>
<DialogTitle>
{editingUser ? '编辑用户' : '添加用户'}
</DialogTitle>
<DialogContent>
<Box component="form" onSubmit={editingUser ? handleUpdateUser : handleCreateUser}>
<TextField
autoFocus
margin="dense"
name="username"
label="用户名"
type="text"
fullWidth
variant="outlined"
value={formData.username}
onChange={(e) => setFormData({...formData, username: e.target.value})}
required
/>
<TextField
margin="dense"
name="email"
label="邮箱"
type="email"
fullWidth
variant="outlined"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
required
/>
<TextField
margin="dense"
name="password"
label="密码"
type="password"
fullWidth
variant="outlined"
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
required={!editingUser}
/>
<DialogActions>
<Button onClick={handleCloseDialog}>取消</Button>
<Button
type="submit"
variant="contained"
color="primary"
>
{editingUser ? '更新' : '创建'}
</Button>
</DialogActions>
</Box>
</DialogContent>
</Dialog>
</Box>
);
};
export default UserList;
主应用组件
// src/App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Box from '@mui/material/Box';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import UserList from './components/UserList';
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
全栈应用演示
</Typography>
</Toolbar>
</AppBar>
<Routes>
<Route path="/" element={<UserList />} />
</Routes>
</Box>
</Router>
</ThemeProvider>
);
}
export default App;
状态管理与数据流
React Context API 实践
// src/context/UserContext.jsx
import React, { createContext, useContext, useReducer } from 'react';
const UserContext = createContext();
const userReducer = (state, action) => {
switch (action.type) {
case 'SET_USERS':
return { ...state, users: action.payload };
case 'ADD_USER':
return { ...state, users: [...state.users, action.payload] };
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.payload.id ? action.payload : user
)
};
case 'DELETE_USER':
return {
...state,
users: state.users.filter(user => user.id !== action.payload)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
};
export const UserProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, {
users: [],
loading: false,
error: null
});
const fetchUsers = async () => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const data = await userService.getUsers();
dispatch({ type: 'SET_USERS', payload: data });
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
const createUser = async (userData) => {
try {
const newUser = await userService.createUser(userData);
dispatch({ type: 'ADD_USER', payload: newUser });
return newUser;
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
}
};
const updateUser = async (id, userData) => {
try {
const updatedUser = await userService.updateUser(id, userData);
dispatch({ type: 'UPDATE_USER', payload: updatedUser });
return updatedUser;
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
}
};
const deleteUser = async (id) => {
try {
await userService.deleteUser(id);
dispatch({ type: 'DELETE_USER', payload: id });
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
}
};
return (
<UserContext.Provider value={{
...state,
fetchUsers,
createUser,
updateUser,
deleteUser
}}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};
性能优化与最佳实践
API调用优化
// src/hooks/useApi.js
import { useState, useEffect } from 'react';
export const useApi = (apiFunction, dependencies = []) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await apiFunction();
if (!isCancelled) {
setData(result);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
isCancelled = true;
};
}, dependencies);
const refetch = async () => {
try {
const result = await apiFunction();
setData(result);
} catch (err) {
setError(err.message);
}
};
return { data, loading, error, refetch };
};
// 使用示例
// const { data: users, loading, error, refetch } = useApi(() => userService.getUsers());
组件性能优化
// src/components/UserCard.jsx
import React, { memo } from 'react';
import {
Card,
CardContent,
Typography,
Button,
CardActions
} from '@mui/material';
const UserCard = memo(({ user, onEdit, onDelete }) => {
return (
<Card sx={{ minWidth: 275, mb: 2 }}>
<CardContent>
<Typography variant="h6" component="div">
{user.username}
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
{user.email}
</Typography>
<Typography variant="body2">
创建时间: {new Date(user.createdAt).toLocaleString()}
</Typography>
</CardContent>
<CardActions>
<Button size="small" onClick={() => onEdit(user)}>编辑</Button>
<Button size="small" color="error" onClick={() => onDelete(user.id)}>删除</Button>
</CardActions>
</Card>
);
});
export default UserCard;
安全性考虑
CORS配置
// CorsConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
JWT认证实现
// JwtAuthenticationFilter.java
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
部署与运维
Docker容器化部署
# backend/Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# frontend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Docker Compose配置
# docker-compose.yml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
- db

评论 (0)