
文章目录
一、为什么选择Spring Boot:极速开发的秘密
作为.NET转Java的开发者,您会发现Spring Boot与ASP.NET Core在理念上惊人的相似,但Spring Boot的“约定优于配置”哲学将其发挥到极致。Spring Boot可以:
- 在30秒内启动一个可运行的REST API
- 通过自动配置减少80%的配置工作
- 提供生产就绪的功能(监控、健康检查、指标)
- 拥有全球最大的Java生态支持
二、极速启动:三步创建第一个REST接口
2.1 项目初始化
使用Spring Initializr(start.spring.io)或IDE内置工具创建项目,只需选择:
- Spring Web:RESTful支持
- Spring Data JPA:数据库操作(可选)
- Validation:数据验证(可选)
2.2 基础代码示例
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@Valid @RequestBody UserDTO userDTO) {
return userService.create(userDTO);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id,
@Valid @RequestBody UserDTO userDTO) {
return userService.update(id, userDTO);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
三、Spring Boot RESTful核心详解
3.1 控制器层最佳实践
3.1.1 RESTful资源设计原则
// 好的RESTful设计示例
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
// GET /api/v1/orders - 获取所有订单
@GetMapping
public List<Order> getAllOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return orderService.getOrders(page, size);
}
// GET /api/v1/orders/{id} - 获取特定订单
@GetMapping("/{id}")
public Order getOrderById(@PathVariable Long id) {
return orderService.getById(id);
}
// GET /api/v1/orders/{id}/items - 获取订单项(子资源)
@GetMapping("/{id}/items")
public List<OrderItem> getOrderItems(@PathVariable Long id) {
return orderService.getOrderItems(id);
}
// POST /api/v1/orders - 创建订单
@PostMapping
public ResponseEntity<Order> createOrder(@Valid @RequestBody OrderDTO dto) {
Order created = orderService.create(dto);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
}
3.1.2 高级请求处理技巧
// 1. 多条件查询参数处理
@GetMapping("/search")
public List<User> searchUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "desc") String direction) {
Specification<User> spec = UserSpecifications.search(name, email, startDate, endDate);
Sort sort = direction.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
return userService.search(spec, sort);
}
// 2. 文件上传处理
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("category") String category) {
if (file.isEmpty()) {
throw new BadRequestException("文件不能为空");
}
String filePath = fileStorageService.store(file, category);
return ResponseEntity.ok("文件上传成功: " + filePath);
}
// 3. 请求/响应体压缩
@PostMapping("/compress")
public ResponseEntity<byte[]> handleCompressedData(
@RequestBody byte[] compressedData) throws IOException {
byte[] decompressed = CompressionUtils.decompress(compressedData);
// 处理数据...
byte[] responseData = "处理成功".getBytes();
byte[] compressedResponse = CompressionUtils.compress(responseData);
return ResponseEntity.ok()
.header("Content-Encoding", "gzip")
.body(compressedResponse);
}
3.2 服务层设计与实现
3.2.1 服务层架构模式
// 1. 基础服务接口
public interface UserService {
UserDTO createUser(UserCreateDTO dto);
UserDTO updateUser(Long id, UserUpdateDTO dto);
UserDTO getUserById(Long id);
Page<UserDTO> getUsers(UserQueryDTO query, Pageable pageable);
void deleteUser(Long id);
}
// 2. 服务实现(使用@Transactional)
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final CacheService cacheService;
private final EmailService emailService;
// 构造器注入(推荐方式)
public UserServiceImpl(UserRepository userRepository,
UserMapper userMapper,
CacheService cacheService,
EmailService emailService) {
this.userRepository = userRepository;
this.userMapper = userMapper;
this.cacheService = cacheService;
this.emailService = emailService;
}
@Override
public UserDTO createUser(UserCreateDTO dto) {
log.info("创建用户: {}", dto.getEmail());
// 验证逻辑
if (userRepository.existsByEmail(dto.getEmail())) {
throw new BusinessException("邮箱已存在");
}
// DTO转实体
User user = userMapper.toEntity(dto);
user.setStatus(UserStatus.ACTIVE);
user.setCreatedAt(LocalDateTime.now());
// 保存到数据库
User savedUser = userRepository.save(user);
// 清理缓存
cacheService.evict("users", savedUser.getId());
// 发送欢迎邮件(异步)
emailService.sendWelcomeEmail(savedUser.getEmail());
// 返回DTO
return userMapper.toDTO(savedUser);
}
@Override
@Transactional(readOnly = true)
public UserDTO getUserById(Long id) {
// 先尝试从缓存获取
String cacheKey = "user:" + id;
UserDTO cached = cacheService.get(cacheKey, UserDTO.class);
if (cached != null) {
return cached;
}
// 缓存未命中,查询数据库
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在"));
UserDTO dto = userMapper.toDTO(user);
// 放入缓存
cacheService.put(cacheKey, dto, Duration.ofMinutes(30));
return dto;
}
@Override
@Transactional(readOnly = true)
public Page<UserDTO> getUsers(UserQueryDTO query, Pageable pageable) {
// 构建查询条件
Specification<User> spec = buildSpecification(query);
// 执行分页查询
Page<User> userPage = userRepository.findAll(spec, pageable);
// 转换为DTO
return userPage.map(userMapper::toDTO);
}
private Specification<User> buildSpecification(UserQueryDTO query) {
return (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.hasText(query.getName())) {
predicates.add(criteriaBuilder.like(
root.get("name"), "%" + query.getName() + "%"
));
}
if (query.getStatus() != null) {
predicates.add(criteriaBuilder.equal(
root.get("status"), query.getStatus()
));
}
if (query.getStartDate() != null) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(
root.get("createdAt"), query.getStartDate()
));
}
if (query.getEndDate() != null) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(
root.get("createdAt"), query.getEndDate()
));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
}
3.2.2 业务逻辑与事务管理
// 复杂业务事务管理示例
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final NotificationService notificationService;
/**
* 创建订单的复杂业务流程
* 使用@Transactional管理事务边界
*/
@Transactional(rollbackFor = Exception.class)
public OrderDTO createOrder(OrderCreateDTO dto) {
// 1. 验证库存
inventoryService.checkStock(dto.getItems());
// 2. 扣减库存(独立事务)
inventoryService.deductStock(dto.getItems());
try {
// 3. 创建订单
Order order = createOrderEntity(dto);
order = orderRepository.save(order);
// 4. 调用支付(外部系统,需要补偿机制)
paymentService.processPayment(order);
// 5. 更新订单状态
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 6. 发送通知(异步,不影响主事务)
notificationService.sendOrderConfirmation(order);
return convertToDTO(order);
} catch (PaymentException e) {
// 支付失败,恢复库存
inventoryService.restoreStock(dto.getItems());
throw new BusinessException("支付失败: " + e.getMessage());
}
}
/**
* 使用编程式事务处理特殊场景
*/
public void batchUpdateOrderStatus(List<Long> orderIds, OrderStatus status) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
for (Long orderId : orderIds) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("订单不存在"));
// 记录状态变更历史
order.addStatusHistory(order.getStatus(), status, "批量更新");
// 更新状态
order.setStatus(status);
orderRepository.save(order);
// 每处理100条记录提交一次,防止事务过大
if (orderIds.indexOf(orderId) % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
return null;
});
}
}
3.3 数据传输对象设计
3.3.1 DTO模式实现
// 1. 请求DTO(验证注解)
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$",
message = "密码必须至少8位,包含字母和数字")
private String password;
@NotNull(message = "角色不能为空")
private UserRole role;
@PhoneNumber // 自定义验证注解
private String phone;
}
// 2. 响应DTO(嵌套对象)
@Data
@Builder
public class UserDetailDTO {
private Long id;
private String username;
private String email;
private UserStatus status;
private LocalDateTime createdAt;
private List<UserAddressDTO> addresses;
private UserProfileDTO profile;
}
// 3. 查询参数DTO
@Data
public class UserQueryDTO {
private String username;
private String email;
private UserStatus status;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String sortField;
private Sort.Direction sortDirection;
public Pageable toPageable() {
if (sortField != null && sortDirection != null) {
return PageRequest.of(0, 20, Sort.by(sortDirection, sortField));
}
return PageRequest.of(0, 20);
}
}
3.3.2 MapStruct映射器
// 1. 映射器接口
@Mapper(componentModel = "spring",
uses = {AddressMapper.class, ProfileMapper.class},
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// 简单映射
User toEntity(UserCreateDTO dto);
UserDTO toDTO(User entity);
// 更新映射(忽略空值)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntity(UserUpdateDTO dto, @MappingTarget User entity);
// 列表映射
List<UserDTO> toDTOList(List<User> entities);
// 分页映射
default Page<UserDTO> toDTOPage(Page<User> page) {
return page.map(this::toDTO);
}
// 自定义映射方法
@AfterMapping
default void afterMapping(UserCreateDTO dto, @MappingTarget User user) {
if (user.getPassword() != null) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
user.setCreatedAt(LocalDateTime.now());
}
}
// 2. 复杂映射配置
@Mapper(componentModel = "spring")
public interface OrderMapper {
@Mapping(source = "customer.id", target = "customerId")
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "items", target = "orderItems")
@Mapping(target = "totalAmount", expression = "java(calculateTotal(order))")
OrderDTO toDTO(Order order);
default BigDecimal calculateTotal(Order order) {
return order.getItems().stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
四、全局处理与高级特性
4.1 全局异常处理机制
4.1.1 统一异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理验证异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("验证失败")
.message("请求参数无效")
.errors(errors)
.path(getRequestPath())
.build();
return ResponseEntity.badRequest().body(response);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex, HttpServletRequest request) {
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("业务错误")
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
// 处理资源不存在异常
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(
ResourceNotFoundException ex, HttpServletRequest request) {
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.NOT_FOUND.value())
.error("资源未找到")
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
// 处理所有未捕获的异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(
Exception ex, HttpServletRequest request) {
log.error("未处理的异常: ", ex);
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("服务器内部错误")
.message("系统繁忙,请稍后重试")
.path(request.getRequestURI())
.build();
return ResponseEntity.internalServerError().body(response);
}
private String getRequestPath() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes instanceof ServletRequestAttributes) {
return ((ServletRequestAttributes) attributes).getRequest().getRequestURI();
}
return null;
}
}
// 错误响应DTO
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private List<String> errors;
private String path;
}
4.1.2 自定义异常体系
// 基础业务异常
public abstract class BaseException extends RuntimeException {
private final String code;
private final Map<String, Object> data;
public BaseException(String code, String message) {
super(message);
this.code = code;
this.data = new HashMap<>();
}
public BaseException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.data = new HashMap<>();
}
public BaseException withData(String key, Object value) {
this.data.put(key, value);
return this;
}
public abstract HttpStatus getHttpStatus();
}
// 具体业务异常
public class BusinessException extends BaseException {
public BusinessException(String message) {
super("BUSINESS_ERROR", message);
}
public BusinessException(String code, String message) {
super(code, message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.CONFLICT;
}
}
public class ValidationException extends BaseException {
public ValidationException(String message) {
super("VALIDATION_ERROR", message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_REQUEST;
}
}
public class AuthenticationException extends BaseException {
public AuthenticationException(String message) {
super("AUTH_ERROR", message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.UNAUTHORIZED;
}
}
4.2 数据验证高级技巧
4.2.1 自定义验证注解
// 1. 自定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
String message() default "手机号码格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String region() default "CN"; // 支持不同地区
}
// 2. 验证器实现
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
private String region;
private static final Map<String, Pattern> REGION_PATTERNS = new HashMap<>();
static {
// 中国大陆手机号
REGION_PATTERNS.put("CN", Pattern.compile("^1[3-9]\\d{9}$"));
// 美国手机号
REGION_PATTERNS.put("US", Pattern.compile("^\\(?([0-9]{3})\\)?[-.\\s]?([0-9]{3})[-.\\s]?([0-9]{4})$"));
// 香港手机号
REGION_PATTERNS.put("HK", Pattern.compile("^[569]\\d{3}\\d{4}$"));
}
@Override
public void initialize(PhoneNumber constraintAnnotation) {
this.region = constraintAnnotation.region();
}
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
if (phoneNumber == null) {
return true; // 使用@NotNull处理空值
}
Pattern pattern = REGION_PATTERNS.get(region);
if (pattern == null) {
throw new IllegalArgumentException("不支持的地区: " + region);
}
return pattern.matcher(phoneNumber).matches();
}
}
// 3. 使用自定义注解
@Data
public class ContactDTO {
@NotBlank(message = "姓名不能为空")
private String name;
@PhoneNumber(region = "CN", message = "请输入有效的中国大陆手机号")
private String phone;
@Email(message = "邮箱格式不正确")
private String email;
// 跨字段验证
@AssertTrue(message = "至少提供一种联系方式")
public boolean isContactInfoProvided() {
return StringUtils.hasText(phone) || StringUtils.hasText(email);
}
}
4.2.2 分组验证
// 1. 定义验证组
public interface ValidationGroups {
interface Create {}
interface Update {}
interface Patch {}
}
// 2. 在DTO中使用分组
@Data
public class UserDTO {
@Null(groups = Create.class, message = "ID必须为空")
@NotNull(groups = {Update.class, Patch.class}, message = "ID不能为空")
private Long id;
@NotBlank(groups = Create.class, message = "用户名不能为空")
@Size(min = 3, max = 50, groups = {Create.class, Update.class})
private String username;
@Email(groups = {Create.class, Update.class})
private String email;
@NotBlank(groups = Create.class, message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).{8,}$",
groups = Create.class)
private String password;
@NotNull(groups = Create.class)
private UserRole role;
}
// 3. 在控制器中使用分组验证
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Validated(ValidationGroups.Create.class)
@RequestBody UserDTO userDTO) {
// 创建逻辑
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Validated(ValidationGroups.Update.class)
@RequestBody UserDTO userDTO) {
// 更新逻辑
}
@PatchMapping("/{id}")
public ResponseEntity<UserDTO> patchUser(
@PathVariable Long id,
@Validated(ValidationGroups.Patch.class)
@RequestBody UserDTO userDTO) {
// 部分更新逻辑
}
}
五、安全与认证授权
5.1 Spring Security集成
5.1.1 安全配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final AccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(REST API通常不需要)
.csrf().disable()
// 会话管理(无状态)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 异常处理
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
// 授权配置
.authorizeHttpRequests(auth -> auth
// 公开接口
.requestMatchers(
"/api/auth/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/health"
).permitAll()
// 需要认证的接口
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// 其他接口需要认证
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
// 记住我功能(可选)
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400) // 24小时
.and()
// 安全头配置
.headers(headers -> headers
.contentSecurityPolicy("default-src 'self'")
.frameOptions().sameOrigin()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
}
5.1.2 JWT认证实现
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 从请求头获取Token
String token = extractToken(request);
if (token != null && tokenProvider.validateToken(token)) {
// 从Token中获取用户名
String username = tokenProvider.getUsernameFromToken(token);
// 加载用户详情
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("无法设置用户认证", ex);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private Long expiration;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
logger.error("无效的JWT签名");
} catch (MalformedJwtException ex) {
logger.error("无效的JWT令牌");
} catch (ExpiredJwtException ex) {
logger.error("JWT令牌已过期");
} catch (UnsupportedJwtException ex) {
logger.error("不支持的JWT令牌");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims字符串为空");
}
return false;
}
}
5.2 方法级安全控制
5.2.1 基于注解的权限控制
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 基于角色的访问控制
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ProductDTO createProduct(@Valid @RequestBody ProductCreateDTO dto) {
return productService.create(dto);
}
// 基于权限的访问控制
@PreAuthorize("hasAuthority('PRODUCT_READ')")
@GetMapping("/{id}")
public ProductDTO getProduct(@PathVariable Long id) {
return productService.getById(id);
}
// 基于表达式的复杂权限控制
@PreAuthorize("hasRole('ADMIN') or @productSecurity.isOwner(#id, authentication)")
@PutMapping("/{id}")
public ProductDTO updateProduct(
@PathVariable Long id,
@Valid @RequestBody ProductUpdateDTO dto) {
return productService.update(id, dto);
}
// 方法调用后权限检查
@PostAuthorize("returnObject.status != 'DELETED'")
@GetMapping("/secure/{id}")
public ProductDTO getSecureProduct(@PathVariable Long id) {
return productService.getById(id);
}
// 基于过滤器的权限控制
@PreFilter("filterObject.ownerId == authentication.principal.id")
@PostMapping("/batch")
public List<ProductDTO> createProducts(
@RequestBody List<ProductCreateDTO> products) {
return productService.createBatch(products);
}
// 方法调用后过滤
@PostFilter("filterObject.price > 100")
@GetMapping("/expensive")
public List<ProductDTO> getExpensiveProducts() {
return productService.getAll();
}
}
// 自定义安全表达式处理器
@Component("productSecurity")
public class ProductSecurity {
private final ProductRepository productRepository;
public boolean isOwner(Long productId, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String username = authentication.getName();
Optional<Product> product = productRepository.findById(productId);
return product.isPresent() &&
product.get().getCreatedBy().equals(username);
}
public boolean canView(Product product, Authentication authentication) {
// 复杂的业务逻辑判断
if (product.isPublic()) {
return true;
}
if (authentication == null) {
return false;
}
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return product.getOwners().contains(userDetails.getUsername()) ||
userDetails.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
5.2.2 权限缓存与性能优化
@Configuration
@EnableCaching
public class SecurityCacheConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
cacheManager.setCacheNames(Arrays.asList(
"userDetails",
"permissions",
"aclCache"
));
return cacheManager;
}
}
@Service
public class CachingUserDetailsService implements UserDetailsService {
private final UserDetailsService delegate;
private final CacheManager cacheManager;
@Cacheable(value = "userDetails", key = "#username")
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return delegate.loadUserByUsername(username);
}
@CacheEvict(value = "userDetails", key = "#username")
public void evictUserCache(String username) {
// 缓存清除
}
}
// 权限缓存服务
@Service
public class PermissionCacheService {
@Cacheable(value = "permissions", key = "#userId + ':' + #resource")
public boolean hasPermission(Long userId, String resource, String action) {
// 从数据库查询权限
return permissionRepository.existsByUserIdAndResourceAndAction(
userId, resource, action);
}
@CacheEvict(value = "permissions", allEntries = true)
public void clearAllPermissionCache() {
// 清除所有权限缓存
}
}
六、API文档与测试
6.1 OpenAPI/Swagger集成
6.1.1 Springdoc OpenAPI配置
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "订单管理系统API",
version = "1.0.0",
description = "订单管理系统REST API文档",
contact = @Contact(
name = "技术支持",
email = "support@example.com",
url = "https://example.com"
),
license = @License(
name = "Apache 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0"
),
termsOfService = "https://example.com/terms"
),
servers = {
@Server(
url = "http://localhost:8080",
description = "开发环境"
),
@Server(
url = "https://api.example.com",
description = "生产环境"
)
},
security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSchemas("ErrorResponse", new Schema<ErrorResponse>()
.type("object")
.addProperty("timestamp", new Schema<String>()
.type("string")
.format("date-time"))
.addProperty("status", new Schema<Integer>()
.type("integer"))
.addProperty("error", new Schema<String>()
.type("string"))
.addProperty("message", new Schema<String>()
.type("string"))
.addProperty("path", new Schema<String>()
.type("string")))
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.externalDocs(new ExternalDocumentation()
.description("更多文档")
.url("https://docs.example.com"));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
}
6.1.2 控制器文档注解
@RestController
@RequestMapping("/api/orders")
@Tag(name = "订单管理", description = "订单相关操作")
public class OrderController {
@Operation(
summary = "获取订单列表",
description = "分页获取订单列表,支持多种查询条件",
parameters = {
@Parameter(name = "page", description = "页码,从0开始", example = "0"),
@Parameter(name = "size", description = "每页大小", example = "20"),
@Parameter(name = "status", description = "订单状态"),
@Parameter(name = "startDate", description = "开始日期", example = "2024-01-01"),
@Parameter(name = "endDate", description = "结束日期", example = "2024-12-31")
}
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "成功获取订单列表",
content = @Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = OrderDTO.class))
)
),
@ApiResponse(
responseCode = "401",
description = "未授权访问"
),
@ApiResponse(
responseCode = "403",
description = "权限不足"
)
})
@GetMapping
@PreAuthorize("hasRole('USER')")
public Page<OrderDTO> getOrders(
@ParameterObject Pageable pageable,
@ParameterObject OrderQueryDTO query) {
return orderService.getOrders(query, pageable);
}
@Operation(
summary = "创建订单",
description = "创建新订单,需要商品信息和收货地址"
)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<OrderDTO> createOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "订单创建信息",
required = true,
content = @Content(
schema = @Schema(implementation = OrderCreateDTO.class)
)
)
@Valid @RequestBody OrderCreateDTO dto) {
OrderDTO created = orderService.createOrder(dto);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@Operation(
summary = "获取订单详情",
description = "根据ID获取订单详细信息"
)
@GetMapping("/{id}")
public OrderDTO getOrder(
@Parameter(description = "订单ID", required = true, example = "123")
@PathVariable Long id) {
return orderService.getOrderById(id);
}
@Operation(
summary = "更新订单状态",
description = "更新订单状态,支持取消、完成等操作"
)
@PatchMapping("/{id}/status")
public OrderDTO updateOrderStatus(
@Parameter(description = "订单ID", required = true)
@PathVariable Long id,
@RequestBody OrderStatusUpdateDTO dto) {
return orderService.updateStatus(id, dto);
}
@Operation(
summary = "导出订单",
description = "导出订单数据为Excel文件"
)
@GetMapping("/export")
public void exportOrders(
@ParameterObject OrderQueryDTO query,
HttpServletResponse response) throws IOException {
List<OrderDTO> orders = orderService.exportOrders(query);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=orders.xlsx");
exportService.exportToExcel(orders, response.getOutputStream());
}
}
6.2 全面测试策略
6.2.1 单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private EmailService emailService;
@InjectMocks
private UserServiceImpl userService;
private UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void createUser_ShouldReturnUserDTO_WhenValidInput() {
// 准备测试数据
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setUsername("testuser");
createDTO.setEmail("test@example.com");
createDTO.setPassword("password123");
User user = new User();
user.setId(1L);
user.setUsername("testuser");
user.setEmail("test@example.com");
// 设置Mock行为
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(user);
// 执行测试
UserDTO result = userService.createUser(createDTO);
// 验证结果
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("testuser", result.getUsername());
assertEquals("test@example.com", result.getEmail());
// 验证交互
verify(userRepository).existsByEmail("test@example.com");
verify(passwordEncoder).encode("password123");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("test@example.com");
}
@Test
void createUser_ShouldThrowException_WhenEmailExists() {
// 准备测试数据
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setEmail("existing@example.com");
// 设置Mock行为
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
// 执行测试并验证异常
BusinessException exception = assertThrows(
BusinessException.class,
() -> userService.createUser(createDTO)
);
assertEquals("邮箱已存在", exception.getMessage());
}
@Test
@DisplayName("根据ID获取用户 - 用户存在")
void getUserById_ShouldReturnUser_WhenUserExists() {
// 准备测试数据
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setUsername("testuser");
// 设置Mock行为
when(userRepository.findById(userId))
.thenReturn(Optional.of(user));
// 执行测试
UserDTO result = userService.getUserById(userId);
// 验证结果
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("testuser", result.getUsername());
}
@Test
@DisplayName("根据ID获取用户 - 用户不存在")
void getUserById_ShouldThrowException_WhenUserNotFound() {
// 设置Mock行为
when(userRepository.findById(anyLong()))
.thenReturn(Optional.empty());
// 执行测试并验证异常
ResourceNotFoundException exception = assertThrows(
ResourceNotFoundException.class,
() -> userService.getUserById(1L)
);
assertEquals("用户不存在", exception.getMessage());
}
@ParameterizedTest
@CsvSource({
"1, admin, ADMIN",
"2, user, USER",
"3, manager, MANAGER"
})
void getUserById_WithDifferentUsers_ShouldReturnCorrectUser(
Long id, String username, UserRole role) {
User user = new User();
user.setId(id);
user.setUsername(username);
user.setRole(role);
when(userRepository.findById(id)).thenReturn(Optional.of(user));
UserDTO result = userService.getUserById(id);
assertEquals(id, result.getId());
assertEquals(username, result.getUsername());
assertEquals(role, result.getRole());
}
@Test
void updateUser_ShouldUpdateAndReturnUser() {
// 准备测试数据
Long userId = 1L;
UserUpdateDTO updateDTO = new UserUpdateDTO();
updateDTO.setUsername("updateduser");
updateDTO.setEmail("updated@example.com");
User existingUser = new User();
existingUser.setId(userId);
existingUser.setUsername("olduser");
existingUser.setEmail("old@example.com");
User updatedUser = new User();
updatedUser.setId(userId);
updatedUser.setUsername("updateduser");
updatedUser.setEmail("updated@example.com");
// 设置Mock行为
when(userRepository.findById(userId))
.thenReturn(Optional.of(existingUser));
when(userRepository.save(any(User.class)))
.thenReturn(updatedUser);
// 执行测试
UserDTO result = userService.updateUser(userId, updateDTO);
// 验证结果
assertEquals("updateduser", result.getUsername());
assertEquals("updated@example.com", result.getEmail());
// 验证交互
verify(userRepository).findById(userId);
verify(userRepository).save(existingUser);
assertEquals("updateduser", existingUser.getUsername());
assertEquals("updated@example.com", existingUser.getEmail());
}
}
6.2.2 集成测试
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
@Transactional
class UserControllerIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void createUser_ShouldReturnCreatedUser() throws Exception {
// 准备测试数据
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setUsername("integrationtest");
createDTO.setEmail("integration@test.com");
createDTO.setPassword("Password123!");
createDTO.setRole(UserRole.USER);
// 执行请求
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDTO)))
// 验证响应
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.username").value("integrationtest"))
.andExpect(jsonPath("$.email").value("integration@test.com"))
.andExpect(jsonPath("$.role").value("USER"));
// 验证数据库
Optional<User> savedUser = userRepository.findByEmail("integration@test.com");
assertTrue(savedUser.isPresent());
assertEquals("integrationtest", savedUser.get().getUsername());
}
@Test
void createUser_ShouldReturnBadRequest_WhenInvalidInput() throws Exception {
// 准备无效的测试数据
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setEmail("invalid-email");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDTO)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").exists());
}
@Test
void getUser_ShouldReturnUser_WhenUserExists() throws Exception {
// 准备测试数据
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("encodedPassword");
user.setRole(UserRole.USER);
user = userRepository.save(user);
// 执行请求
mockMvc.perform(get("/api/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(user.getId()))
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("test@example.com"));
}
@Test
void getUser_ShouldReturnNotFound_WhenUserNotExists() throws Exception {
mockMvc.perform(get("/api/users/{id}", 999))
.andExpect(status().isNotFound());
}
@Test
void updateUser_ShouldUpdateUser() throws Exception {
// 创建测试用户
User user = new User();
user.setUsername("olduser");
user.setEmail("old@example.com");
user.setPassword("encodedPassword");
user.setRole(UserRole.USER);
user = userRepository.save(user);
// 准备更新数据
UserUpdateDTO updateDTO = new UserUpdateDTO();
updateDTO.setUsername("updateduser");
updateDTO.setEmail("updated@example.com");
// 执行更新请求
mockMvc.perform(put("/api/users/{id}", user.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("updateduser"))
.andExpect(jsonPath("$.email").value("updated@example.com"));
// 验证数据库更新
Optional<User> updatedUser = userRepository.findById(user.getId());
assertTrue(updatedUser.isPresent());
assertEquals("updateduser", updatedUser.get().getUsername());
assertEquals("updated@example.com", updatedUser.get().getEmail());
}
@Test
void deleteUser_ShouldDeleteUser() throws Exception {
// 创建测试用户
User user = new User();
user.setUsername("tobedeleted");
user.setEmail("delete@example.com");
user.setPassword("encodedPassword");
user = userRepository.save(user);
// 执行删除请求
mockMvc.perform(delete("/api/users/{id}", user.getId()))
.andExpect(status().isNoContent());
// 验证用户已删除
assertFalse(userRepository.existsById(user.getId()));
}
@Test
void getUsers_ShouldReturnPaginatedUsers() throws Exception {
// 创建测试数据
for (int i = 1; i <= 25; i++) {
User user = new User();
user.setUsername("user" + i);
user.setEmail("user" + i + "@example.com");
user.setPassword("password" + i);
userRepository.save(user);
}
// 执行分页查询
mockMvc.perform(get("/api/users")
.param("page", "0")
.param("size", "10")
.param("sort", "username,asc"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content.length()").value(10))
.andExpect(jsonPath("$.totalPages").value(3))
.andExpect(jsonPath("$.totalElements").value(25))
.andExpect(jsonPath("$.first").value(true))
.andExpect(jsonPath("$.last").value(false));
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void adminEndpoint_ShouldBeAccessible_ForAdminUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void adminEndpoint_ShouldBeForbidden_ForNonAdminUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
}
七、部署与监控
7.1 Docker容器化部署
7.1.1 Dockerfile配置
# 构建阶段
FROM maven:3.8.4-openjdk-17-slim AS build
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY pom.xml .
COPY src ./src
# 下载依赖并构建(利用Docker层缓存)
RUN mvn dependency:go-offline
RUN mvn clean package -DskipTests
# 运行时阶段
FROM openjdk:17-jdk-slim
# 安装必要的工具
RUN apt-get update && apt-get install -y \
curl \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# 设置时区
ENV TZ=Asia/Shanghai
# 创建非root用户
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring
# 设置工作目录
WORKDIR /app
# 从构建阶段复制JAR文件
COPY --from=build /app/target/*.jar app.jar
# 暴露端口
EXPOSE 8080
# JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp"
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app/app.jar"]
7.1.2 Docker Compose配置
version: '3.8'
services:
# 主应用服务
app:
build: .
container_name: spring-app
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/appdb
- SPRING_DATASOURCE_USERNAME=appuser
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# PostgreSQL数据库
postgres:
image: postgres:15-alpine
container_name: app-postgres
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
# Redis缓存
redis:
image: redis:7-alpine
container_name: app-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Nginx反向代理
nginx:
image: nginx:alpine
container_name: app-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
depends_on:
- app
networks:
- backend
restart: unless-stopped
# 监控系统 (Prometheus + Grafana)
prometheus:
image: prom/prometheus:latest
container_name: app-prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports:
- "9090:9090"
networks:
- backend
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: app-grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- "3000:3000"
networks:
- backend
restart: unless-stopped
networks:
backend:
driver: bridge
volumes:
postgres_data:
redis_data:
prometheus_data:
grafana_data:
7.2 性能监控与指标
7.2.1 Spring Boot Actuator配置
# application-prod.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /actuator
cors:
allowed-origins: "*"
allowed-methods: GET,POST
endpoint:
health:
show-details: when_authorized
roles: ADMIN
probes:
enabled: true
groups:
liveness:
include: livenessState
readiness:
include: readinessState
metrics:
enabled: true
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
step: 1m
distribution:
percentiles-histogram:
"[http.server.requests]": true
sla:
"[http.server.requests]": 10ms, 50ms, 100ms, 200ms, 500ms, 1s, 2s
info:
env:
enabled: true
java:
enabled: true
os:
enabled: true
tracing:
sampling:
probability: 0.1
server:
port: 8081 # 单独的监控端口
7.2.2 自定义健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Health health() {
try {
// 检查数据库连接
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
if (result != null && result == 1) {
// 检查数据库性能
Map<String, Object> details = new HashMap<>();
// 获取连接池信息
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) dataSource;
details.put("activeConnections", hikari.getHikariPoolMXBean().getActiveConnections());
details.put("idleConnections", hikari.getHikariPoolMXBean().getIdleConnections());
details.put("totalConnections", hikari.getHikariPoolMXBean().getTotalConnections());
}
return Health.up()
.withDetails(details)
.build();
}
return Health.down()
.withDetail("error", "数据库查询返回异常结果")
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.withDetail("error", "数据库连接失败: " + e.getMessage())
.build();
}
}
}
@Component
public class CacheHealthIndicator implements HealthIndicator {
private final CacheManager cacheManager;
public CacheHealthIndicator(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null && cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
@SuppressWarnings("unchecked")
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache =
(com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache.getNativeCache();
Map<String, Object> cacheStats = new HashMap<>();
cacheStats.put("estimatedSize", caffeineCache.estimatedSize());
cacheStats.put("stats", caffeineCache.stats());
details.put(cacheName, cacheStats);
}
}
return Health.up()
.withDetails(details)
.build();
}
}
@Component
public class ExternalServiceHealthIndicator extends AbstractHealthIndicator {
private final RestTemplate restTemplate;
private final String serviceUrl;
public ExternalServiceHealthIndicator(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
this.serviceUrl = "https://api.external-service.com/health";
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
try {
ResponseEntity<String> response = restTemplate.getForEntity(serviceUrl, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
builder.up()
.withDetail("status", response.getStatusCode().value())
.withDetail("responseTime", "正常");
} else {
builder.down()
.withDetail("status", response.getStatusCode().value())
.withDetail("error", "外部服务返回异常状态码");
}
} catch (ResourceAccessException e) {
builder.down()
.withException(e)
.withDetail("error", "连接外部服务超时");
} catch (Exception e) {
builder.down()
.withException(e)
.withDetail("error", "检查外部服务健康状态时发生异常");
}
}
}
八、性能优化与最佳实践
8.1 数据库性能优化
8.1.1 连接池配置
# application-prod.yml
spring:
datasource:
hikari:
# 连接池配置
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 性能优化
auto-commit: false
connection-test-query: SELECT 1
validation-timeout: 5000
leak-detection-threshold: 60000
# 连接属性
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
useLocalSessionState: true
rewriteBatchedStatements: true
cacheResultSetMetadata: true
cacheServerConfiguration: true
elideSetAutoCommits: true
maintainTimeStats: false
8.1.2 JPA性能优化
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository",
repositoryBaseClass = CustomJpaRepositoryImpl.class
)
@EnableTransactionManagement
public class JpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
DataSource dataSource) {
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.jdbc.batch_size", 50);
properties.put("hibernate.order_inserts", true);
properties.put("hibernate.order_updates", true);
properties.put("hibernate.batch_versioned_data", true);
properties.put("hibernate.query.in_clause_parameter_padding", true);
properties.put("hibernate.default_batch_fetch_size", 16);
properties.put("hibernate.max_fetch_depth", 3);
properties.put("hibernate.jdbc.fetch_size", 100);
// 生产环境禁用DDL自动更新
properties.put("hibernate.hbm2ddl.auto", "validate");
return builder
.dataSource(dataSource)
.packages("com.example.entity")
.persistenceUnit("default")
.properties(properties)
.build();
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
// 自定义Repository实现
@NoRepositoryBean
public class CustomJpaRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>
implements CustomJpaRepository<T, ID> {
private final EntityManager entityManager;
public CustomJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
@Transactional(readOnly = true)
public List<T> findAllWithPagination(int offset, int limit, Sort sort) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<T> query = cb.createQuery(getDomainClass());
Root<T> root = query.from(getDomainClass());
query.select(root);
if (sort != null) {
List<Order> orders = new ArrayList<>();
sort.forEach(order -> {
if (order.isAscending()) {
orders.add(cb.asc(root.get(order.getProperty())));
} else {
orders.add(cb.desc(root.get(order.getProperty())));
}
});
query.orderBy(orders);
}
return entityManager.createQuery(query)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
@Override
@Transactional
public int batchInsert(List<T> entities) {
int batchSize = 50;
int count = 0;
for (int i = 0; i < entities.size(); i++) {
entityManager.persist(entities.get(i));
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear();
count += batchSize;
}
}
entityManager.flush();
entityManager.clear();
return count + (entities.size() % batchSize);
}
}
8.2 缓存策略优化
8.2.1 多级缓存配置
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 全局缓存配置
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30))
.maximumSize(10000)
.recordStats());
// 自定义缓存配置
Map<String, Caffeine<Object, Object>> cacheConfigs = new HashMap<>();
// 用户缓存 - 较短时间,高频访问
cacheConfigs.put("users", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(1000)
.recordStats());
// 商品缓存 - 较长时间,低频更新
cacheConfigs.put("products", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(2))
.maximumSize(5000)
.recordStats());
// 配置缓存 - 永不过期,手动刷新
cacheConfigs.put("config", Caffeine.newBuilder()
.maximumSize(100)
.recordStats());
cacheManager.setCacheSpecification(cacheConfigs);
return cacheManager;
}
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 不同缓存不同配置
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("users", config.entryTtl(Duration.ofMinutes(30)));
cacheConfigs.put("products", config.entryTtl(Duration.ofHours(2)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
@Bean
public CacheManager multiLevelCacheManager(
CacheManager localCacheManager,
CacheManager redisCacheManager) {
return new MultiLevelCacheManager(localCacheManager, redisCacheManager);
}
}
// 多级缓存实现
public class MultiLevelCacheManager implements CacheManager {
private final CacheManager localCacheManager; // L1: Caffeine
private final CacheManager redisCacheManager; // L2: Redis
public MultiLevelCacheManager(CacheManager localCacheManager,
CacheManager redisCacheManager) {
this.localCacheManager = localCacheManager;
this.redisCacheManager = redisCacheManager;
}
@Override
public Cache getCache(String name) {
Cache localCache = localCacheManager.getCache(name);
Cache remoteCache = redisCacheManager.getCache(name);
return new MultiLevelCache(name, localCache, remoteCache);
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new HashSet<>();
names.addAll(localCacheManager.getCacheNames());
names.addAll(redisCacheManager.getCacheNames());
return names;
}
}
class MultiLevelCache implements Cache {
private final String name;
private final Cache localCache;
private final Cache remoteCache;
public MultiLevelCache(String name, Cache localCache, Cache remoteCache) {
this.name = name;
this.localCache = localCache;
this.remoteCache = remoteCache;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return remoteCache.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
// 先查本地缓存
ValueWrapper value = localCache.get(key);
if (value != null) {
return value;
}
// 本地缓存未命中,查Redis
value = remoteCache.get(key);
if (value != null) {
// 回写到本地缓存
localCache.put(key, value.get());
}
return value;
}
@Override
public <T> T get(Object key, Class<T> type) {
// 实现类似get方法
T value = localCache.get(key, type);
if (value != null) {
return value;
}
value = remoteCache.get(key, type);
if (value != null) {
localCache.put(key, value);
}
return value;
}
@Override
public void put(Object key, Object value) {
// 同时写入两级缓存
localCache.put(key, value);
remoteCache.put(key, value);
}
@Override
public void evict(Object key) {
// 同时清除两级缓存
localCache.evict(key);
remoteCache.evict(key);
}
@Override
public void clear() {
localCache.clear();
remoteCache.clear();
}
}
九、生产环境最佳实践
9.1 应用配置管理
9.1.1 多环境配置
# application.yml (基础配置)
spring:
application:
name: order-service
profiles:
active: @spring.profiles.active@
# 日志配置
logging:
level:
com.example: INFO
org.springframework.web: INFO
org.hibernate.SQL: WARN
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30
# application-dev.yml (开发环境)
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
path: /h2-console
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUG
# application-prod.yml (生产环境)
server:
port: 8080
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024
tomcat:
max-connections: 10000
max-threads: 200
min-spare-threads: 10
connection-timeout: 5000
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
jpa:
hibernate:
ddl-auto: validate
show-sql: false
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# 生产环境日志配置
logging:
level:
com.example: INFO
org.springframework.web: WARN
org.hibernate: WARN
logstash:
enabled: true
host: ${LOGSTASH_HOST}
port: ${LOGSTASH_PORT}
9.2 监控告警配置
9.2.1 Prometheus监控配置
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
labels:
application: 'order-service'
environment: 'production'
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
9.2.2 告警规则配置
# alert_rules.yml
groups:
- name: spring_boot_alerts
rules:
- alert: HighErrorRate
expr: rate(http_server_requests_seconds_count{status=~"5..",uri!~".*actuator.*"}[5m]) > 0.05
for: 2m
labels:
severity: critical
team: backend
annotations:
summary: "高错误率报警"
description: "{{ $labels.instance }}的错误率超过5% (当前值: {{ $value }})"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "高响应时间报警"
description: "{{ $labels.instance }}的95%响应时间超过1秒 (当前值: {{ $value }}s)"
- alert: ServiceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
team: backend
annotations:
summary: "服务宕机报警"
description: "{{ $labels.instance }}服务已宕机"
- alert: HighMemoryUsage
expr: (sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"})) > 0.8
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "高内存使用率报警"
description: "{{ $labels.instance }}内存使用率超过80% (当前值: {{ $value }})"
- alert: HighCPULoad
expr: system_cpu_usage > 0.8
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "高CPU使用率报警"
description: "{{ $labels.instance }}CPU使用率超过80% (当前值: {{ $value }})"
十、学习路径规划
10.1 初学者入门路径(1-2周)
-
掌握Spring Boot基础
- 理解Spring Boot自动配置原理
- 掌握RESTful API设计原则
- 学习Spring MVC注解使用
-
完成第一个CRUD项目
- 创建用户管理系统
- 实现增删改查接口
- 添加数据验证
10.2 进阶提升路径(3-4周)
-
深入Spring生态
- 学习Spring Security实现安全控制
- 掌握Spring Data JPA高级特性
- 了解Spring Cache缓存机制
-
项目实战
- 实现电商系统核心模块
- 集成第三方服务(支付、短信)
- 添加API文档和单元测试
10.3 专家精通路径(2-3个月)
-
性能优化
- JVM调优实践
- 数据库查询优化
- 缓存策略设计
-
微服务架构
- Spring Cloud学习
- 服务注册与发现
- 分布式事务处理
-
生产实践
- Docker容器化部署
- CI/CD流水线搭建
- 监控告警系统建设
通过以上系统学习路径,可以从Spring Boot新手逐步成长为RESTful API开发专家,掌握企业级应用开发的全套技能栈。

7924





