Spring Boot是目前最流行的Java企业级应用开发框架,本文将通过一个完整的项目实例,从环境搭建到项目部署,全面讲解Spring Boot的核心特性和实战应用。
1. Spring Boot概述
1.1 什么是Spring Boot?
Spring Boot是由Pivotal团队提供的框架,其设计目的是简化Spring应用的创建、配置和部署过程。
Spring Boot的核心优势:
- 快速开发:开箱即用,零配置
- 内嵌服务器:无需部署到外部Tomcat
- 自动配置:根据类路径自动配置
- 健康检查:内置Actuator监控
- 微服务友好:天然支持微服务架构
1.2 Spring Boot版本选择
| 版本 | 特性 | 适用场景 |
|---|---|---|
| 2.7.x | 稳定版本 | 生产环境推荐 |
| 3.x | Java 17+、Spring 6 | 新项目推荐 |
本文基于Spring Boot 3.x版本
2. 环境搭建
2.1 开发工具配置
JDK版本要求:
bash
# 检查Java版本
java -version
# 需要Java 17或更高版本
openjdk version "17.0.8" 2023-07-18
Maven配置(settings.xml):
xml
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Aliyun Maven</name>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>jdk-17</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</profile>
</profiles>
</settings>
2.2 Spring Initializr创建项目
方式一:在线创建
访问 https://start.spring.io/,选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.x
- Packaging: Jar
- Java: 17+
方式二:命令行创建
bash
# 使用Spring Boot CLI
spring init demo-project \
--dependencies=web,data-jpa,mysql,security \
--groupId=com.example \
--artifactId=demo \
--package-name=com.example.demo \
--version=1.0.0
# 或者使用cURL
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,mysql \
-d groupId=com.example \
-d artifactId=demo \
-d name=demo \
-d baseDir=demo \
-o demo.zip
3. 项目结构详解
3.1 标准项目结构
demo/
├── pom.xml # Maven配置
├── mvnw # Maven Wrapper脚本
├── mvnw.cmd
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── demo/
│ │ │ ├── DemoApplication.java # 启动类
│ │ │ ├── config/ # 配置类
│ │ │ ├── controller/ # 控制器层
│ │ │ ├── service/ # 服务层
│ │ │ ├── repository/ # 数据访问层
│ │ │ ├── entity/ # 实体类
│ │ │ ├── dto/ # 数据传输对象
│ │ │ ├── mapper/ # MyBatis映射器
│ │ │ ├── security/ # 安全配置
│ │ │ └── exception/ # 异常处理
│ │ └── resources/
│ │ ├── application.yml # 配置文件
│ │ ├── application-dev.yml # 开发环境配置
│ │ ├── application-prod.yml # 生产环境配置
│ │ ├── static/ # 静态资源
│ │ └── templates/ # 模板文件
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── demo/
│ └── DemoApplicationTests.java # 单元测试
└── target/ # 编译输出目录
3.2 核心配置文件
application.yml:
yaml
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: demo
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
pool-name: DemoHikariCP
max-lifetime: 1800000
connection-timeout: 30000
# JPA配置
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true
open-in-view: false
# Redis配置
data:
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
# MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
root: INFO
com.example.demo: DEBUG
org.hibernate.SQL: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Actuator配置
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
4. 核心功能实现
4.1 启动类配置
DemoApplication.java:
java
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@MapperScan("com.example.demo.mapper")
@EnableCaching // 开启缓存
@EnableAsync // 开启异步
@EnableScheduling // 开启定时任务
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("🚀 Demo Application Started Successfully!");
}
}
4.2 实体类设计
User.java:
java
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Entity
@Table(name = "users")
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false)
private String password;
@Column(length = 100)
private String email;
@Column(length = 20)
private String phone;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "created_at", updatable = false)
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();
}
public enum UserStatus {
ACTIVE, // 活跃
INACTIVE, // 非活跃
LOCKED // 锁定
}
}
4.3 Repository层
UserRepository.java:
java
package com.example.demo.repository;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// 根据用户名查询
Optional<User> findByUsername(String username);
// 根据邮箱查询
Optional<User> findByEmail(String email);
// 根据状态查询
List<User> findByStatus(UserStatus status);
// 分页查询
Page<User> findByStatus(UserStatus status, Pageable pageable);
// 自定义查询
@Query("SELECT u FROM User u WHERE u.username = :username AND u.status = :status")
Optional<User> findByUsernameAndStatus(
@Param("username") String username,
@Param("status") UserStatus status
);
// 统计用户数量
long countByStatus(UserStatus status);
// 模糊查询
List<User> findByUsernameContainingIgnoreCase(String username);
// 原生查询
@Query(value = "SELECT * FROM users WHERE created_at > :startDate ORDER BY created_at DESC",
nativeQuery = true)
List<User> findRecentUsers(@Param("startDate") LocalDateTime startDate);
// 批量删除
void deleteByStatus(UserStatus status);
}
4.4 Service层
UserService.java:
java
package com.example.demo.service;
import com.example.demo.dto.UserCreateDTO;
import com.example.demo.dto.UserUpdateDTO;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
/**
* 查询所有用户(带缓存)
*/
@Cacheable(value = "users", key = "'all'")
public List<User> findAll() {
log.info("查询所有用户");
return userRepository.findAll();
}
/**
* 分页查询用户
*/
public Page<User> findByPage(UserStatus status, Pageable pageable) {
return userRepository.findByStatus(status, pageable);
}
/**
* 根据ID查询用户
*/
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
log.info("查询用户: {}", id);
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
}
/**
* 根据用户名查询
*/
public User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new BusinessException("用户不存在"));
}
/**
* 创建用户
*/
@Transactional
@CacheEvict(value = "users", allEntries = true)
public User create(UserCreateDTO createDTO) {
log.info("创建用户: {}", createDTO.getUsername());
// 检查用户名是否已存在
if (userRepository.findByUsername(createDTO.getUsername()).isPresent()) {
throw new BusinessException("用户名已存在");
}
// 检查邮箱是否已存在
if (createDTO.getEmail() != null &&
userRepository.findByEmail(createDTO.getEmail()).isPresent()) {
throw new BusinessException("邮箱已被注册");
}
// 创建用户
User user = new User();
user.setUsername(createDTO.getUsername());
user.setPassword(passwordEncoder.encode(createDTO.getPassword()));
user.setEmail(createDTO.getEmail());
user.setPhone(createDTO.getPhone());
user.setStatus(UserStatus.ACTIVE);
return userRepository.save(user);
}
/**
* 更新用户
*/
@Transactional
@CacheEvict(value = "users", key = "#id")
public User update(Long id, UserUpdateDTO updateDTO) {
log.info("更新用户: {}", id);
User user = findById(id);
if (updateDTO.getEmail() != null) {
user.setEmail(updateDTO.getEmail());
}
if (updateDTO.getPhone() != null) {
user.setPhone(updateDTO.getPhone());
}
if (updateDTO.getStatus() != null) {
user.setStatus(updateDTO.getStatus());
}
return userRepository.save(user);
}
/**
* 删除用户
*/
@Transactional
@CacheEvict(value = "users", allEntries = true)
public void delete(Long id) {
log.info("删除用户: {}", id);
if (!userRepository.existsById(id)) {
throw new BusinessException("用户不存在");
}
userRepository.deleteById(id);
}
/**
* 批量删除
*/
@Transactional
@CacheEvict(value = "users", allEntries = true)
public void batchDelete(List<Long> ids) {
log.info("批量删除用户: {}", ids);
userRepository.deleteAllById(ids);
}
}
4.5 Controller层
UserController.java:
java
package com.example.demo.controller;
import com.example.demo.dto.*;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 获取所有用户
*/
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
/**
* 分页查询用户
*/
@GetMapping("/page")
public ResponseEntity<Page<User>> getUsersByPage(
@RequestParam(required = false) UserStatus status,
@PageableDefault(size = 10, sort = "createdAt") Pageable pageable) {
return ResponseEntity.ok(userService.findByPage(status, pageable));
}
/**
* 根据ID查询用户
*/
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateDTO createDTO) {
User user = userService.create(createDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateDTO updateDTO) {
return ResponseEntity.ok(userService.update(id, updateDTO));
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
/**
* 批量删除用户
*/
@DeleteMapping("/batch")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> batchDeleteUsers(@RequestBody List<Long> ids) {
userService.batchDelete(ids);
return ResponseEntity.noContent().build();
}
}
5. 数据访问层
5.1 JPA动态查询
UserSpecification.java:
java
package com.example.demo.specification;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class UserSpecification {
public static Specification<User> withSearch(String username,
UserStatus status,
LocalDateTime startDate,
LocalDateTime endDate) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (username != null && !username.isEmpty()) {
predicates.add(criteriaBuilder.like(
criteriaBuilder.lower(root.get("username")),
"%" + username.toLowerCase() + "%"
));
}
if (status != null) {
predicates.add(criteriaBuilder.equal(root.get("status"), status));
}
if (startDate != null) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(
root.get("createdAt"), startDate
));
}
if (endDate != null) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(
root.get("createdAt"), endDate
));
}
query.orderBy(criteriaBuilder.desc(root.get("createdAt")));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
}
6. 安全性配置
6.1 Spring Security配置
SecurityConfig.java:
java
package com.example.demo.config;
import com.example.demo.security.JwtAuthenticationFilter;
import com.example.demo.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
@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(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/users/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
7. 缓存配置
7.1 Redis缓存配置
RedisConfig.java:
java
package com.example.demo.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
8. 定时任务
8.1 定时任务示例
java
package com.example.demo.task;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class ScheduledTasks {
private final UserRepository userRepository;
/**
* 每天凌晨1点执行:清理不活跃用户
*/
@Scheduled(cron = "0 0 1 * * ?")
public void cleanupInactiveUsers() {
log.info("开始清理不活跃用户: {}", LocalDateTime.now());
List<User> inactiveUsers = userRepository
.findByStatus(UserStatus.INACTIVE);
log.info("清理完成,共清理 {} 个用户", inactiveUsers.size());
}
/**
* 每小时执行:发送统计报告
*/
@Scheduled(fixedRate = 3600000) // 1小时
public void sendHourlyReport() {
log.info("生成每小时统计报告: {}", LocalDateTime.now());
long userCount = userRepository.count();
log.info("当前用户总数: {}", userCount);
}
/**
* 每天零点:数据同步
*/
@Scheduled(cron = "0 0 0 * * ?")
public void dailyDataSync() {
log.info("开始每日数据同步: {}", LocalDateTime.now());
}
}
9. 异常处理
9.1 全局异常处理器
GlobalExceptionHandler.java:
java
package com.example.demo.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
log.error("业务异常: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(error);
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
ValidationErrorResponse response = new ValidationErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"参数校验失败",
errors,
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(response);
}
/**
* 认证异常
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponse> handleBadCredentialsException(
BadCredentialsException ex) {
log.error("认证失败: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
HttpStatus.UNAUTHORIZED.value(),
"用户名或密码错误",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
/**
* 权限不足异常
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(
AccessDeniedException ex) {
log.error("权限不足: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
HttpStatus.FORBIDDEN.value(),
"权限不足,无法访问此资源",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
/**
* 其他异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("系统异常", ex);
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"系统繁忙,请稍后重试",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
10. 测试
10.1 单元测试
UserServiceTest.java:
java
package com.example.demo.service;
import com.example.demo.dto.UserCreateDTO;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
private User testUser;
private UserCreateDTO createDTO;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setId(1L);
testUser.setUsername("testuser");
testUser.setPassword("encodedPassword");
testUser.setEmail("test@example.com");
testUser.setStatus(UserStatus.ACTIVE);
testUser.setCreatedAt(LocalDateTime.now());
createDTO = new UserCreateDTO();
createDTO.setUsername("newuser");
createDTO.setPassword("password123");
createDTO.setEmail("new@example.com");
}
@Test
void findById_WhenUserExists_ReturnsUser() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
// When
User result = userService.findById(1L);
// Then
assertNotNull(result);
assertEquals("testuser", result.getUsername());
verify(userRepository).findById(1L);
}
@Test
void findById_WhenUserNotExists_ThrowsException() {
// Given
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// When & Then
assertThrows(BusinessException.class, () -> {
userService.findById(999L);
});
verify(userRepository).findById(999L);
}
@Test
void create_WhenUsernameExists_ThrowsException() {
// Given
when(userRepository.findByUsername("newuser")).thenReturn(Optional.of(testUser));
// When & Then
assertThrows(BusinessException.class, () -> {
userService.create(createDTO);
});
verify(userRepository).findByUsername("newuser");
verify(userRepository, never()).save(any());
}
@Test
void create_WhenValidData_ReturnsUser() {
// Given
when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty());
when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(2L);
return user;
});
// When
User result = userService.create(createDTO);
// Then
assertNotNull(result);
assertEquals("newuser", result.getUsername());
assertEquals("new@example.com", result.getEmail());
assertEquals(UserStatus.ACTIVE, result.getStatus());
verify(passwordEncoder).encode("password123");
verify(userRepository).save(any(User.class));
}
}
11. 项目部署
11.1 Maven构建
bash
# 清理构建
./mvnw clean
# 编译项目
./mvnw compile
# 运行测试
./mvnw test
# 打包
./mvnw package -DskipTests
# 生成Docker镜像
./mvnw spring-boot:build-image
11.2 Docker部署
Dockerfile:
dockerfile
# 构建阶段
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
# 运行阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# 设置时区
RUN apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml:
yaml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/demo
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root_password
- SPRING_REDIS_HOST=redis
depends_on:
- db
- redis
networks:
- demo-network
db:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=demo
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- demo-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- demo-network
networks:
demo-network:
volumes:
mysql_data:
redis_data:
11.3 Kubernetes部署
deployment.yaml:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: demo:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: MYSQL_HOST
valueFrom:
configMapKeyRef:
name: demo-config
key: mysql-host
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: demo-secrets
key: mysql-password
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: demo-service
spec:
selector:
app: demo-app
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
12. 最佳实践总结
12.1 项目结构最佳实践
分层清晰:
- controller/ # 控制器层(处理HTTP请求)
- service/ # 服务层(业务逻辑)
- repository/ # 数据访问层(JPA/MyBatis)
- entity/ # 实体类
- dto/ # 数据传输对象
- mapper/ # MyBatis映射器
- config/ # 配置类
- security/ # 安全配置
- exception/ # 异常处理
- utils/ # 工具类
12.2 开发最佳实践
java
// ✅ 好的实践
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
}
}
// ❌ 避免的实践
@Service
public class BadUserService {
private UserRepository userRepository;
public BadUserService(UserRepository userRepository) {
this.userRepository = userRepository; // 应该用Lombok @RequiredArgsConstructor
}
public User findById(Long id) {
User user = userRepository.findById(id).get(); // 可能抛出NoSuchElementException
return user;
}
}
12.3 性能优化建议
- 使用连接池:HikariCP是高性能连接池
- 开启缓存:减少数据库访问
- 批量操作:减少数据库交互次数
- 异步处理:提高响应速度
- 懒加载:按需加载数据
- 索引优化:合理设计数据库索引
12.4 安全性建议
- 使用JWT:无状态认证,适合分布式环境
- **密码加密