從入門到精通詳解Java中的RESTful接口開發(fā)
一、為什么選擇Spring Boot:極速開發(fā)的秘密
作為.NET轉(zhuǎn)Java的開發(fā)者,您會發(fā)現(xiàn)Spring Boot與ASP.NET Core在理念上驚人的相似,但Spring Boot的“約定優(yōu)于配置”哲學(xué)將其發(fā)揮到極致。Spring Boot可以:
- 在30秒內(nèi)啟動一個可運行的REST API
- 通過自動配置減少80%的配置工作
- 提供生產(chǎn)就緒的功能(監(jiān)控、健康檢查、指標(biāo))
- 擁有全球最大的Java生態(tài)支持
二、極速啟動:三步創(chuàng)建第一個REST接口
2.1 項目初始化
使用Spring Initializr(start.spring.io)或IDE內(nèi)置工具創(chuàng)建項目,只需選擇:
- Spring Web:RESTful支持
- Spring Data JPA:數(shù)據(jù)庫操作(可選)
- Validation:數(shù)據(jù)驗證(可選)
2.2 基礎(chǔ)代碼示例
@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資源設(shè)計原則
// 好的RESTful設(shè)計示例
@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 - 創(chuàng)建訂單
@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. 多條件查詢參數(shù)處理
@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. 請求/響應(yīng)體壓縮
@PostMapping("/compress")
public ResponseEntity<byte[]> handleCompressedData(
@RequestBody byte[] compressedData) throws IOException {
byte[] decompressed = CompressionUtils.decompress(compressedData);
// 處理數(shù)據(jù)...
byte[] responseData = "處理成功".getBytes();
byte[] compressedResponse = CompressionUtils.compress(responseData);
return ResponseEntity.ok()
.header("Content-Encoding", "gzip")
.body(compressedResponse);
}
3.2 服務(wù)層設(shè)計與實現(xiàn)
3.2.1 服務(wù)層架構(gòu)模式
// 1. 基礎(chǔ)服務(wù)接口
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. 服務(wù)實現(xiàn)(使用@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;
// 構(gòu)造器注入(推薦方式)
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("創(chuàng)建用戶: {}", dto.getEmail());
// 驗證邏輯
if (userRepository.existsByEmail(dto.getEmail())) {
throw new BusinessException("郵箱已存在");
}
// DTO轉(zhuǎn)實體
User user = userMapper.toEntity(dto);
user.setStatus(UserStatus.ACTIVE);
user.setCreatedAt(LocalDateTime.now());
// 保存到數(shù)據(jù)庫
User savedUser = userRepository.save(user);
// 清理緩存
cacheService.evict("users", savedUser.getId());
// 發(fā)送歡迎郵件(異步)
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;
}
// 緩存未命中,查詢數(shù)據(jù)庫
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) {
// 構(gòu)建查詢條件
Specification<User> spec = buildSpecification(query);
// 執(zhí)行分頁查詢
Page<User> userPage = userRepository.findAll(spec, pageable);
// 轉(zhuǎn)換為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 業(yè)務(wù)邏輯與事務(wù)管理
// 復(fù)雜業(yè)務(wù)事務(wù)管理示例
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final NotificationService notificationService;
/**
* 創(chuàng)建訂單的復(fù)雜業(yè)務(wù)流程
* 使用@Transactional管理事務(wù)邊界
*/
@Transactional(rollbackFor = Exception.class)
public OrderDTO createOrder(OrderCreateDTO dto) {
// 1. 驗證庫存
inventoryService.checkStock(dto.getItems());
// 2. 扣減庫存(獨立事務(wù))
inventoryService.deductStock(dto.getItems());
try {
// 3. 創(chuàng)建訂單
Order order = createOrderEntity(dto);
order = orderRepository.save(order);
// 4. 調(diào)用支付(外部系統(tǒng),需要補(bǔ)償機(jī)制)
paymentService.processPayment(order);
// 5. 更新訂單狀態(tài)
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 6. 發(fā)送通知(異步,不影響主事務(wù))
notificationService.sendOrderConfirmation(order);
return convertToDTO(order);
} catch (PaymentException e) {
// 支付失敗,恢復(fù)庫存
inventoryService.restoreStock(dto.getItems());
throw new BusinessException("支付失敗: " + e.getMessage());
}
}
/**
* 使用編程式事務(wù)處理特殊場景
*/
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("訂單不存在"));
// 記錄狀態(tài)變更歷史
order.addStatusHistory(order.getStatus(), status, "批量更新");
// 更新狀態(tài)
order.setStatus(status);
orderRepository.save(order);
// 每處理100條記錄提交一次,防止事務(wù)過大
if (orderIds.indexOf(orderId) % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
return null;
});
}
}
3.3 數(shù)據(jù)傳輸對象設(shè)計
3.3.1 DTO模式實現(xiàn)
// 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位,包含字母和數(shù)字")
private String password;
@NotNull(message = "角色不能為空")
private UserRole role;
@PhoneNumber // 自定義驗證注解
private String phone;
}
// 2. 響應(yīng)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. 查詢參數(shù)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. 復(fù)雜映射配置
@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 全局異常處理機(jī)制
4.1.1 統(tǒng)一異常處理器
@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("請求參數(shù)無效")
.errors(errors)
.path(getRequestPath())
.build();
return ResponseEntity.badRequest().body(response);
}
// 處理業(yè)務(wù)異常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex, HttpServletRequest request) {
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("業(yè)務(wù)錯誤")
.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("服務(wù)器內(nèi)部錯誤")
.message("系統(tǒng)繁忙,請稍后重試")
.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;
}
}
// 錯誤響應(yīng)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 自定義異常體系
// 基礎(chǔ)業(yè)務(wù)異常
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();
}
// 具體業(yè)務(wù)異常
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 數(shù)據(jù)驗證高級技巧
4.2.1 自定義驗證注解
// 1. 自定義注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
String message() default "手機(jī)號碼格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String region() default "CN"; // 支持不同地區(qū)
}
// 2. 驗證器實現(xiàn)
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
private String region;
private static final Map<String, Pattern> REGION_PATTERNS = new HashMap<>();
static {
// 中國大陸手機(jī)號
REGION_PATTERNS.put("CN", Pattern.compile("^1[3-9]\\d{9}$"));
// 美國手機(jī)號
REGION_PATTERNS.put("US", Pattern.compile("^\\(?([0-9]{3})\\)?[-.\\s]?([0-9]{3})[-.\\s]?([0-9]{4})$"));
// 香港手機(jī)號
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("不支持的地區(qū): " + region);
}
return pattern.matcher(phoneNumber).matches();
}
}
// 3. 使用自定義注解
@Data
public class ContactDTO {
@NotBlank(message = "姓名不能為空")
private String name;
@PhoneNumber(region = "CN", message = "請輸入有效的中國大陸手機(jī)號")
private String phone;
@Email(message = "郵箱格式不正確")
private String email;
// 跨字段驗證
@AssertTrue(message = "至少提供一種聯(lián)系方式")
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) {
// 創(chuàng)建邏輯
}
@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) {
// 部分更新邏輯
}
}
五、安全與認(rèn)證授權(quán)
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()
// 會話管理(無狀態(tài))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 異常處理
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
// 授權(quán)配置
.authorizeHttpRequests(auth -> auth
// 公開接口
.requestMatchers(
"/api/auth/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/health"
).permitAll()
// 需要認(rèn)證的接口
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// 其他接口需要認(rèn)證
.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認(rèn)證實現(xiàn)
@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);
// 創(chuàng)建認(rèn)證對象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 設(shè)置詳情
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 設(shè)置安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("無法設(shè)置用戶認(rèn)證", 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 基于注解的權(quán)限控制
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 基于角色的訪問控制
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ProductDTO createProduct(@Valid @RequestBody ProductCreateDTO dto) {
return productService.create(dto);
}
// 基于權(quán)限的訪問控制
@PreAuthorize("hasAuthority('PRODUCT_READ')")
@GetMapping("/{id}")
public ProductDTO getProduct(@PathVariable Long id) {
return productService.getById(id);
}
// 基于表達(dá)式的復(fù)雜權(quán)限控制
@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);
}
// 方法調(diào)用后權(quán)限檢查
@PostAuthorize("returnObject.status != 'DELETED'")
@GetMapping("/secure/{id}")
public ProductDTO getSecureProduct(@PathVariable Long id) {
return productService.getById(id);
}
// 基于過濾器的權(quán)限控制
@PreFilter("filterObject.ownerId == authentication.principal.id")
@PostMapping("/batch")
public List<ProductDTO> createProducts(
@RequestBody List<ProductCreateDTO> products) {
return productService.createBatch(products);
}
// 方法調(diào)用后過濾
@PostFilter("filterObject.price > 100")
@GetMapping("/expensive")
public List<ProductDTO> getExpensiveProducts() {
return productService.getAll();
}
}
// 自定義安全表達(dá)式處理器
@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) {
// 復(fù)雜的業(yè)務(wù)邏輯判斷
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 權(quán)限緩存與性能優(yōu)化
@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) {
// 緩存清除
}
}
// 權(quán)限緩存服務(wù)
@Service
public class PermissionCacheService {
@Cacheable(value = "permissions", key = "#userId + ':' + #resource")
public boolean hasPermission(Long userId, String resource, String action) {
// 從數(shù)據(jù)庫查詢權(quán)限
return permissionRepository.existsByUserIdAndResourceAndAction(
userId, resource, action);
}
@CacheEvict(value = "permissions", allEntries = true)
public void clearAllPermissionCache() {
// 清除所有權(quán)限緩存
}
}
六、API文檔與測試
6.1 OpenAPI/Swagger集成
6.1.1 Springdoc OpenAPI配置
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "訂單管理系統(tǒng)API",
version = "1.0.0",
description = "訂單管理系統(tǒng)REST API文檔",
contact = @Contact(
name = "技術(shù)支持",
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 = "開發(fā)環(huán)境"
),
@Server(
url = "https://api.example.com",
description = "生產(chǎn)環(huán)境"
)
},
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 = "訂單相關(guān)操作")
public class OrderController {
@Operation(
summary = "獲取訂單列表",
description = "分頁獲取訂單列表,支持多種查詢條件",
parameters = {
@Parameter(name = "page", description = "頁碼,從0開始", example = "0"),
@Parameter(name = "size", description = "每頁大小", example = "20"),
@Parameter(name = "status", description = "訂單狀態(tài)"),
@Parameter(name = "startDate", description = "開始日期", example = "2024-01-01"),
@Parameter(name = "endDate", description = "結(jié)束日期", 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 = "未授權(quán)訪問"
),
@ApiResponse(
responseCode = "403",
description = "權(quán)限不足"
)
})
@GetMapping
@PreAuthorize("hasRole('USER')")
public Page<OrderDTO> getOrders(
@ParameterObject Pageable pageable,
@ParameterObject OrderQueryDTO query) {
return orderService.getOrders(query, pageable);
}
@Operation(
summary = "創(chuàng)建訂單",
description = "創(chuàng)建新訂單,需要商品信息和收貨地址"
)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<OrderDTO> createOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "訂單創(chuàng)建信息",
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 = "根據(jù)ID獲取訂單詳細(xì)信息"
)
@GetMapping("/{id}")
public OrderDTO getOrder(
@Parameter(description = "訂單ID", required = true, example = "123")
@PathVariable Long id) {
return orderService.getOrderById(id);
}
@Operation(
summary = "更新訂單狀態(tài)",
description = "更新訂單狀態(tài),支持取消、完成等操作"
)
@PatchMapping("/{id}/status")
public OrderDTO updateOrderStatus(
@Parameter(description = "訂單ID", required = true)
@PathVariable Long id,
@RequestBody OrderStatusUpdateDTO dto) {
return orderService.updateStatus(id, dto);
}
@Operation(
summary = "導(dǎo)出訂單",
description = "導(dǎo)出訂單數(shù)據(jù)為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() {
// 準(zhǔn)備測試數(shù)據(jù)
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");
// 設(shè)置Mock行為
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(user);
// 執(zhí)行測試
UserDTO result = userService.createUser(createDTO);
// 驗證結(jié)果
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() {
// 準(zhǔn)備測試數(shù)據(jù)
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setEmail("existing@example.com");
// 設(shè)置Mock行為
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
// 執(zhí)行測試并驗證異常
BusinessException exception = assertThrows(
BusinessException.class,
() -> userService.createUser(createDTO)
);
assertEquals("郵箱已存在", exception.getMessage());
}
@Test
@DisplayName("根據(jù)ID獲取用戶 - 用戶存在")
void getUserById_ShouldReturnUser_WhenUserExists() {
// 準(zhǔn)備測試數(shù)據(jù)
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setUsername("testuser");
// 設(shè)置Mock行為
when(userRepository.findById(userId))
.thenReturn(Optional.of(user));
// 執(zhí)行測試
UserDTO result = userService.getUserById(userId);
// 驗證結(jié)果
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("testuser", result.getUsername());
}
@Test
@DisplayName("根據(jù)ID獲取用戶 - 用戶不存在")
void getUserById_ShouldThrowException_WhenUserNotFound() {
// 設(shè)置Mock行為
when(userRepository.findById(anyLong()))
.thenReturn(Optional.empty());
// 執(zhí)行測試并驗證異常
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() {
// 準(zhǔn)備測試數(shù)據(jù)
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");
// 設(shè)置Mock行為
when(userRepository.findById(userId))
.thenReturn(Optional.of(existingUser));
when(userRepository.save(any(User.class)))
.thenReturn(updatedUser);
// 執(zhí)行測試
UserDTO result = userService.updateUser(userId, updateDTO);
// 驗證結(jié)果
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 {
// 準(zhǔn)備測試數(shù)據(jù)
UserCreateDTO createDTO = new UserCreateDTO();
createDTO.setUsername("integrationtest");
createDTO.setEmail("integration@test.com");
createDTO.setPassword("Password123!");
createDTO.setRole(UserRole.USER);
// 執(zhí)行請求
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDTO)))
// 驗證響應(yīng)
.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"));
// 驗證數(shù)據(jù)庫
Optional<User> savedUser = userRepository.findByEmail("integration@test.com");
assertTrue(savedUser.isPresent());
assertEquals("integrationtest", savedUser.get().getUsername());
}
@Test
void createUser_ShouldReturnBadRequest_WhenInvalidInput() throws Exception {
// 準(zhǔn)備無效的測試數(shù)據(jù)
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 {
// 準(zhǔn)備測試數(shù)據(jù)
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("encodedPassword");
user.setRole(UserRole.USER);
user = userRepository.save(user);
// 執(zhí)行請求
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 {
// 創(chuàng)建測試用戶
User user = new User();
user.setUsername("olduser");
user.setEmail("old@example.com");
user.setPassword("encodedPassword");
user.setRole(UserRole.USER);
user = userRepository.save(user);
// 準(zhǔn)備更新數(shù)據(jù)
UserUpdateDTO updateDTO = new UserUpdateDTO();
updateDTO.setUsername("updateduser");
updateDTO.setEmail("updated@example.com");
// 執(zhí)行更新請求
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"));
// 驗證數(shù)據(jù)庫更新
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 {
// 創(chuàng)建測試用戶
User user = new User();
user.setUsername("tobedeleted");
user.setEmail("delete@example.com");
user.setPassword("encodedPassword");
user = userRepository.save(user);
// 執(zhí)行刪除請求
mockMvc.perform(delete("/api/users/{id}", user.getId()))
.andExpect(status().isNoContent());
// 驗證用戶已刪除
assertFalse(userRepository.existsById(user.getId()));
}
@Test
void getUsers_ShouldReturnPaginatedUsers() throws Exception {
// 創(chuàng)建測試數(shù)據(jù)
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);
}
// 執(zhí)行分頁查詢
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());
}
}
七、部署與監(jiān)控
7.1 Docker容器化部署
7.1.1 Dockerfile配置
# 構(gòu)建階段
FROM maven:3.8.4-openjdk-17-slim AS build
# 設(shè)置工作目錄
WORKDIR /app
# 復(fù)制項目文件
COPY pom.xml .
COPY src ./src
# 下載依賴并構(gòu)建(利用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/*
# 設(shè)置時區(qū)
ENV TZ=Asia/Shanghai
# 創(chuàng)建非root用戶
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring
# 設(shè)置工作目錄
WORKDIR /app
# 從構(gòu)建階段復(fù)制JAR文件
COPY --from=build /app/target/*.jar app.jar
# 暴露端口
EXPOSE 8080
# JVM參數(shù)
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:
# 主應(yīng)用服務(wù)
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數(shù)據(jù)庫
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
# 監(jiān)控系統(tǒng) (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 性能監(jiān)控與指標(biāo)
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 # 單獨的監(jiān)控端口
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 {
// 檢查數(shù)據(jù)庫連接
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
if (result != null && result == 1) {
// 檢查數(shù)據(jù)庫性能
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", "數(shù)據(jù)庫查詢返回異常結(jié)果")
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.withDetail("error", "數(shù)據(jù)庫連接失敗: " + 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", "外部服務(wù)返回異常狀態(tài)碼");
}
} catch (ResourceAccessException e) {
builder.down()
.withException(e)
.withDetail("error", "連接外部服務(wù)超時");
} catch (Exception e) {
builder.down()
.withException(e)
.withDetail("error", "檢查外部服務(wù)健康狀態(tài)時發(fā)生異常");
}
}
}
八、性能優(yōu)化與最佳實踐
8.1 數(shù)據(jù)庫性能優(yōu)化
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
# 性能優(yōu)化
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性能優(yōu)化
@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);
// 生產(chǎn)環(huán)境禁用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實現(xiàn)
@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 緩存策略優(yōu)化
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);
}
}
// 多級緩存實現(xiàn)
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) {
// 實現(xiàn)類似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();
}
}
九、生產(chǎn)環(huán)境最佳實踐
9.1 應(yīng)用配置管理
9.1.1 多環(huán)境配置
# application.yml (基礎(chǔ)配置)
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 (開發(fā)環(huán)境)
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 (生產(chǎn)環(huán)境)
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
# 生產(chǎn)環(huán)境日志配置
logging:
level:
com.example: INFO
org.springframework.web: WARN
org.hibernate: WARN
logstash:
enabled: true
host: ${LOGSTASH_HOST}
port: ${LOGSTASH_PORT}
9.2 監(jiān)控告警配置
9.2.1 Prometheus監(jiān)控配置
# 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 告警規(guī)則配置
# 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% (當(dāng)前值: {{ $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: "高響應(yīng)時間報警"
description: "{{ $labels.instance }}的95%響應(yīng)時間超過1秒 (當(dāng)前值: {{ $value }}s)"
- alert: ServiceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
team: backend
annotations:
summary: "服務(wù)宕機(jī)報警"
description: "{{ $labels.instance }}服務(wù)已宕機(jī)"
- 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: "高內(nèi)存使用率報警"
description: "{{ $labels.instance }}內(nèi)存使用率超過80% (當(dāng)前值: {{ $value }})"
- alert: HighCPULoad
expr: system_cpu_usage > 0.8
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "高CPU使用率報警"
description: "{{ $labels.instance }}CPU使用率超過80% (當(dāng)前值: {{ $value }})"
十、學(xué)習(xí)路徑規(guī)劃
10.1 初學(xué)者入門路徑(1-2周)
掌握Spring Boot基礎(chǔ)
- 理解Spring Boot自動配置原理
- 掌握RESTful API設(shè)計原則
- 學(xué)習(xí)Spring MVC注解使用
完成第一個CRUD項目
- 創(chuàng)建用戶管理系統(tǒng)
- 實現(xiàn)增刪改查接口
- 添加數(shù)據(jù)驗證
10.2 進(jìn)階提升路徑(3-4周)
深入Spring生態(tài)
- 學(xué)習(xí)Spring Security實現(xiàn)安全控制
- 掌握Spring Data JPA高級特性
- 了解Spring Cache緩存機(jī)制
項目實戰(zhàn)
- 實現(xiàn)電商系統(tǒng)核心模塊
- 集成第三方服務(wù)(支付、短信)
- 添加API文檔和單元測試
10.3 專家精通路徑(2-3個月)
性能優(yōu)化
- JVM調(diào)優(yōu)實踐
- 數(shù)據(jù)庫查詢優(yōu)化
- 緩存策略設(shè)計
微服務(wù)架構(gòu)
- Spring Cloud學(xué)習(xí)
- 服務(wù)注冊與發(fā)現(xiàn)
- 分布式事務(wù)處理
生產(chǎn)實踐
- Docker容器化部署
- CI/CD流水線搭建
- 監(jiān)控告警系統(tǒng)建設(shè)
通過以上系統(tǒng)學(xué)習(xí)路徑,可以從Spring Boot新手逐步成長為RESTful API開發(fā)專家,掌握企業(yè)級應(yīng)用開發(fā)的全套技能棧。
到此這篇關(guān)于從入門到精通詳解Java中的RESTful接口開發(fā)的文章就介紹到這了,更多相關(guān)Java RESTful接口開發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mabatis錯誤提示Parameter index out of range的處理方法
這篇文章主要介紹了Mabatis錯誤提示Parameter index out of range 的處理方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08
ThreadPoolExecutor參數(shù)含義及源碼執(zhí)行流程詳解
這篇文章主要為大家介紹了ThreadPoolExecutor參數(shù)含義及源碼執(zhí)行流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
springboot2中session超時,退到登錄頁面方式
這篇文章主要介紹了springboot2中session超時,退到登錄頁面方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01
dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter
這篇文章主要介紹了dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Java實現(xiàn)矩陣加減乘除及轉(zhuǎn)制等運算功能示例
這篇文章主要介紹了Java實現(xiàn)矩陣加減乘除及轉(zhuǎn)制等運算功能,結(jié)合實例形式總結(jié)分析了java常見的矩陣運算實現(xiàn)技巧,需要的朋友可以參考下2018-01-01
SpringBoot中ApplicationEvent的使用步驟詳解
ApplicationEvent類似于MQ,是Spring提供的一種發(fā)布訂閱模式的事件處理方式,本文給大家介紹SpringBoot中ApplicationEvent的使用步驟詳解,感興趣的朋友跟隨小編一起看看吧2024-04-04

