spring-boot

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Spring Boot 3.x - Production-Ready Java Framework

Spring Boot 3.x - 生产级Java框架

Overview

概述

Spring Boot is an opinionated Java framework for building production-ready applications with minimal configuration. It provides auto-configuration, embedded servers, and production-ready features like health checks and metrics.
Key Features:
  • Auto-configuration (sensible defaults)
  • Embedded servers (Tomcat, Jetty, Undertow)
  • Dependency Injection with @Autowired
  • Spring Data JPA for database access
  • Spring Security for authentication/authorization
  • Actuator for production monitoring
  • Built-in testing support
Requirements:
  • Java 17+ (Spring Boot 3.x requires Java 17 minimum)
  • Maven or Gradle
Quick Start:
bash
undefined
Spring Boot是一套约定大于配置的Java框架,仅需少量配置即可构建生产级应用。它提供了自动配置、内置服务器,以及健康检查、指标监控等生产可用特性。
核心特性:
  • 自动配置(合理的默认值)
  • 内置服务器(Tomcat、Jetty、Undertow)
  • 基于@Autowired的依赖注入
  • 用于数据库访问的Spring Data JPA
  • 用于身份认证/授权的Spring Security
  • 用于生产环境监控的Actuator
  • 内置测试支持
运行要求:
  • Java 17+(Spring Boot 3.x最低要求Java 17)
  • Maven 或 Gradle
快速开始:
bash
undefined

Create project from Spring Initializr

从Spring Initializr创建项目

curl https://start.spring.io/starter.zip
-d type=maven-project
-d language=java
-d bootVersion=3.2.0
-d dependencies=web,data-jpa,postgresql,lombok,actuator
-d name=myapp
-o myapp.zip && unzip myapp.zip
curl https://start.spring.io/starter.zip
-d type=maven-project
-d language=java
-d bootVersion=3.2.0
-d dependencies=web,data-jpa,postgresql,lombok,actuator
-d name=myapp
-o myapp.zip && unzip myapp.zip

Run the application

运行应用

cd myapp ./mvnw spring-boot:run
undefined
cd myapp ./mvnw spring-boot:run
undefined

Project Structure

项目结构

src/
├── main/
│   ├── java/com/example/myapp/
│   │   ├── MyappApplication.java      # Main class
│   │   ├── config/                    # @Configuration classes
│   │   ├── controller/                # @RestController classes
│   │   ├── service/                   # @Service classes
│   │   ├── repository/                # @Repository interfaces
│   │   ├── model/                     # Entity classes
│   │   ├── dto/                       # Data Transfer Objects
│   │   └── exception/                 # Exception handlers
│   └── resources/
│       ├── application.yml            # Configuration
│       └── application-{profile}.yml  # Profile-specific config
└── test/
    └── java/com/example/myapp/        # Test classes
src/
├── main/
│   ├── java/com/example/myapp/
│   │   ├── MyappApplication.java      # 启动主类
│   │   ├── config/                    # @Configuration配置类
│   │   ├── controller/                # @RestController控制层类
│   │   ├── service/                   # @Service服务层类
│   │   ├── repository/                # @Repository数据访问层接口
│   │   ├── model/                     # 实体类
│   │   ├── dto/                       # 数据传输对象
│   │   └── exception/                 # 异常处理器
│   └── resources/
│       ├── application.yml            # 通用配置
│       └── application-{profile}.yml  # 环境专属配置
└── test/
    └── java/com/example/myapp/        # 测试类

Core Annotations

核心注解

Application Setup

应用初始化

java
// Main application class
@SpringBootApplication  // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class MyappApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyappApplication.class, args);
    }
}
java
// 应用启动主类
@SpringBootApplication  // 整合了@Configuration、@EnableAutoConfiguration、@ComponentScan三个注解
public class MyappApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyappApplication.class, args);
    }
}

Dependency Injection

依赖注入

java
// Constructor injection (recommended)
@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    // @Autowired optional on single constructor (Spring 4.3+)
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }
}

// With Lombok
@Service
@RequiredArgsConstructor  // Generates constructor for final fields
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
}

// Field injection (avoid in production code)
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;  // Harder to test
}
java
// 构造函数注入(推荐方式)
@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    // Spring 4.3+之后单构造函数无需添加@Autowired
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }
}

// 配合Lombok使用
@Service
@RequiredArgsConstructor  // 自动为final字段生成构造函数
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
}

// 字段注入(生产代码不推荐使用)
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;  // 可测试性较差
}

Component Stereotypes

组件注解类型

java
@Component      // Generic component
@Service        // Business logic layer
@Repository     // Data access layer (enables exception translation)
@Controller     // MVC controller (returns views)
@RestController // REST API controller (returns JSON)
@Configuration  // Configuration class with @Bean methods
java
@Component      // 通用组件注解
@Service        // 业务逻辑层注解
@Repository     // 数据访问层注解(开启异常转换)
@Controller     // MVC控制器注解(返回视图)
@RestController // REST API控制器注解(返回JSON)
@Configuration  // 配置类注解,用于定义@Bean方法

REST Controllers

REST控制器

Basic Controller

基础控制器

java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // GET /api/v1/users
    @GetMapping
    public ResponseEntity<List<UserDto>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }

    // GET /api/v1/users/{id}
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/v1/users
    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDto created = userService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    // PUT /api/v1/users/{id}
    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    // DELETE /api/v1/users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // GET /api/v1/users/search?email=test@example.com
    @GetMapping("/search")
    public ResponseEntity<List<UserDto>> searchUsers(
            @RequestParam(required = false) String email,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return ResponseEntity.ok(userService.search(email, page, size));
    }
}
java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // GET /api/v1/users
    @GetMapping
    public ResponseEntity<List<UserDto>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }

    // GET /api/v1/users/{id}
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/v1/users
    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDto created = userService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    // PUT /api/v1/users/{id}
    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    // DELETE /api/v1/users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // GET /api/v1/users/search?email=test@example.com
    @GetMapping("/search")
    public ResponseEntity<List<UserDto>> searchUsers(
            @RequestParam(required = false) String email,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return ResponseEntity.ok(userService.search(email, page, size));
    }
}

Request/Response DTOs

请求/响应DTO

java
// Request DTO with validation
@Data
public class CreateUserRequest {
    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be 2-100 characters")
    private String name;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
}

// Response DTO
@Data
@Builder
public class UserDto {
    private Long id;
    private String email;
    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public static UserDto fromEntity(User user) {
        return UserDto.builder()
            .id(user.getId())
            .email(user.getEmail())
            .name(user.getName())
            .createdAt(user.getCreatedAt())
            .updatedAt(user.getUpdatedAt())
            .build();
    }
}
java
// 带校验的请求DTO
@Data
public class CreateUserRequest {
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式无效")
    private String email;

    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 100, message = "姓名长度需在2-100个字符之间")
    private String name;

    @NotBlank(message = "密码不能为空")
    @Size(min = 8, message = "密码长度至少为8个字符")
    private String password;
}

// 响应DTO
@Data
@Builder
public class UserDto {
    private Long id;
    private String email;
    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public static UserDto fromEntity(User user) {
        return UserDto.builder()
            .id(user.getId())
            .email(user.getEmail())
            .name(user.getName())
            .createdAt(user.getCreatedAt())
            .updatedAt(user.getUpdatedAt())
            .build();
    }
}

Service Layer

服务层

java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  // Default to read-only transactions
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public List<UserDto> findAll() {
        return userRepository.findAll().stream()
            .map(UserDto::fromEntity)
            .collect(Collectors.toList());
    }

    public Optional<UserDto> findById(Long id) {
        return userRepository.findById(id)
            .map(UserDto::fromEntity);
    }

    @Transactional  // Read-write transaction
    public UserDto create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException(request.getEmail());
        }

        User user = User.builder()
            .email(request.getEmail())
            .name(request.getName())
            .passwordHash(passwordEncoder.encode(request.getPassword()))
            .build();

        return UserDto.fromEntity(userRepository.save(user));
    }

    @Transactional
    public UserDto update(Long id, UpdateUserRequest request) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));

        if (request.getName() != null) {
            user.setName(request.getName());
        }
        if (request.getEmail() != null) {
            user.setEmail(request.getEmail());
        }

        return UserDto.fromEntity(userRepository.save(user));
    }

    @Transactional
    public void delete(Long id) {
        if (!userRepository.existsById(id)) {
            throw new UserNotFoundException(id);
        }
        userRepository.deleteById(id);
    }

    public List<UserDto> search(String email, int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<User> users = email != null
            ? userRepository.findByEmailContainingIgnoreCase(email, pageable)
            : userRepository.findAll(pageable);

        return users.stream()
            .map(UserDto::fromEntity)
            .collect(Collectors.toList());
    }
}
java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  // 默认开启只读事务
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public List<UserDto> findAll() {
        return userRepository.findAll().stream()
            .map(UserDto::fromEntity)
            .collect(Collectors.toList());
    }

    public Optional<UserDto> findById(Long id) {
        return userRepository.findById(id)
            .map(UserDto::fromEntity);
    }

    @Transactional  // 读写事务
    public UserDto create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException(request.getEmail());
        }

        User user = User.builder()
            .email(request.getEmail())
            .name(request.getName())
            .passwordHash(passwordEncoder.encode(request.getPassword()))
            .build();

        return UserDto.fromEntity(userRepository.save(user));
    }

    @Transactional
    public UserDto update(Long id, UpdateUserRequest request) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));

        if (request.getName() != null) {
            user.setName(request.getName());
        }
        if (request.getEmail() != null) {
            user.setEmail(request.getEmail());
        }

        return UserDto.fromEntity(userRepository.save(user));
    }

    @Transactional
    public void delete(Long id) {
        if (!userRepository.existsById(id)) {
            throw new UserNotFoundException(id);
        }
        userRepository.deleteById(id);
    }

    public List<UserDto> search(String email, int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<User> users = email != null
            ? userRepository.findByEmailContainingIgnoreCase(email, pageable)
            : userRepository.findAll(pageable);

        return users.stream()
            .map(UserDto::fromEntity)
            .collect(Collectors.toList());
    }
}

Repository Layer (Spring Data JPA)

数据访问层(Spring Data JPA)

Basic Repository

基础Repository

java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // Derived query methods
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);

    // Paginated queries
    Page<User> findByEmailContainingIgnoreCase(String email, Pageable pageable);

    // Custom JPQL query
    @Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
    List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);

    // Native SQL query
    @Query(value = "SELECT * FROM users WHERE email ILIKE %:email%", nativeQuery = true)
    List<User> searchByEmail(@Param("email") String email);

    // Modifying query
    @Modifying
    @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
    int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 派生查询方法
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);

    // 分页查询
    Page<User> findByEmailContainingIgnoreCase(String email, Pageable pageable);

    // 自定义JPQL查询
    @Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
    List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);

    // 原生SQL查询
    @Query(value = "SELECT * FROM users WHERE email ILIKE %:email%", nativeQuery = true)
    List<User> searchByEmail(@Param("email") String email);

    // 修改类查询
    @Modifying
    @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
    int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}

Entity Class

实体类

java
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(name = "password_hash", nullable = false)
    private String passwordHash;

    @Column(nullable = false)
    @Builder.Default
    private boolean active = true;

    @Column(name = "created_at", nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    @UpdateTimestamp
    private LocalDateTime updatedAt;

    // Relationships
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<Post> posts = new ArrayList<>();

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    @Builder.Default
    private Set<Role> roles = new HashSet<>();
}
java
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(name = "password_hash", nullable = false)
    private String passwordHash;

    @Column(nullable = false)
    @Builder.Default
    private boolean active = true;

    @Column(name = "created_at", nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    @UpdateTimestamp
    private LocalDateTime updatedAt;

    // 关联关系
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<Post> posts = new ArrayList<>();

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    @Builder.Default
    private Set<Role> roles = new HashSet<>();
}

Configuration

配置

application.yml

application.yml

yaml
spring:
  application:
    name: myapp

  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:password}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate  # none, validate, update, create, create-drop
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        default_schema: public

  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}

server:
  port: ${PORT:8080}
  servlet:
    context-path: /api
yaml
spring:
  application:
    name: myapp

  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:password}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate  # 可选值:none, validate, update, create, create-drop
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        default_schema: public

  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}

server:
  port: ${PORT:8080}
  servlet:
    context-path: /api

Actuator endpoints

Actuator端点配置

management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when_authorized
management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when_authorized

Custom properties

自定义属性

app: jwt: secret: ${JWT_SECRET:your-secret-key} expiration-ms: 86400000
undefined
app: jwt: secret: ${JWT_SECRET:your-secret-key} expiration-ms: 86400000
undefined

Profile-Specific Configuration

环境专属配置

yaml
undefined
yaml
undefined

application-dev.yml

application-dev.yml 开发环境配置

spring: jpa: show-sql: true h2: console: enabled: true
logging: level: com.example.myapp: DEBUG org.springframework.web: DEBUG

spring: jpa: show-sql: true h2: console: enabled: true
logging: level: com.example.myapp: DEBUG org.springframework.web: DEBUG

application-prod.yml

application-prod.yml 生产环境配置

spring: jpa: show-sql: false properties: hibernate: generate_statistics: false
logging: level: com.example.myapp: INFO org.springframework.web: WARN
undefined
spring: jpa: show-sql: false properties: hibernate: generate_statistics: false
logging: level: com.example.myapp: INFO org.springframework.web: WARN
undefined

Configuration Properties Class

配置属性类

java
@Configuration
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtProperties {
    private String secret;
    private long expirationMs;
}

// Usage
@Service
@RequiredArgsConstructor
public class JwtService {
    private final JwtProperties jwtProperties;

    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getEmail())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationMs()))
            .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
            .compact();
    }
}
java
@Configuration
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtProperties {
    private String secret;
    private long expirationMs;
}

// 使用示例
@Service
@RequiredArgsConstructor
public class JwtService {
    private final JwtProperties jwtProperties;

    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getEmail())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationMs()))
            .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
            .compact();
    }
}

Exception Handling

异常处理

Global Exception Handler

全局异常处理器

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // Handle validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());

        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .message("Validation failed")
            .errors(errors)
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.badRequest().body(response);
    }

    // Handle not found exceptions
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.NOT_FOUND.value())
            .message(ex.getMessage())
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    // Handle business logic exceptions
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.CONFLICT.value())
            .message(ex.getMessage())
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
    }

    // Catch-all handler
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
        log.error("Unexpected error occurred", ex);

        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .message("An unexpected error occurred")
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

// Error response DTO
@Data
@Builder
public class ErrorResponse {
    private int status;
    private String message;
    private List<String> errors;
    private LocalDateTime timestamp;
}

// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with id: %d", resource, id));
    }
}

public class UserNotFoundException extends ResourceNotFoundException {
    public UserNotFoundException(Long id) {
        super("User", id);
    }
}
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 处理校验错误
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());

        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .message("参数校验失败")
            .errors(errors)
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.badRequest().body(response);
    }

    // 处理资源不存在异常
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.NOT_FOUND.value())
            .message(ex.getMessage())
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    // 处理业务逻辑异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.CONFLICT.value())
            .message(ex.getMessage())
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
    }

    // 全局兜底异常处理器
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
        log.error("发生未知错误", ex);

        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .message("发生未知错误")
            .timestamp(LocalDateTime.now())
            .build();

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

// 错误响应DTO
@Data
@Builder
public class ErrorResponse {
    private int status;
    private String message;
    private List<String> errors;
    private LocalDateTime timestamp;
}

// 自定义异常
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s不存在,ID:%d", resource, id));
    }
}

public class UserNotFoundException extends ResourceNotFoundException {
    public UserNotFoundException(Long id) {
        super("用户", id);
    }
}

Spring Security

Spring Security

Security Configuration (Spring Security 6.x)

安全配置(Spring Security 6.x)

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
            throws Exception {
        return config.getAuthenticationManager();
    }
}
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
            throws Exception {
        return config.getAuthenticationManager();
    }
}

JWT Filter

JWT过滤器

java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userEmail = jwtService.extractUsername(jwt);

        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities());

                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}
java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userEmail = jwtService.extractUsername(jwt);

        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities());

                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Actuator Endpoints

Actuator端点

yaml
undefined
yaml
undefined

Built-in endpoints

内置端点配置

management: endpoints: web: exposure: include: health,info,metrics,prometheus,env base-path: /actuator endpoint: health: show-details: when_authorized probes: enabled: true # Kubernetes liveness/readiness probes info: env: enabled: true
management: endpoints: web: exposure: include: health,info,metrics,prometheus,env base-path: /actuator endpoint: health: show-details: when_authorized probes: enabled: true # 开启Kubernetes存活性/就绪性探针 info: env: enabled: true

Application info

应用信息配置

info: app: name: ${spring.application.name} version: '@project.version@' java: version: ${java.version}

**Common Actuator Endpoints**:
- `GET /actuator/health` - Application health
- `GET /actuator/health/liveness` - Kubernetes liveness probe
- `GET /actuator/health/readiness` - Kubernetes readiness probe
- `GET /actuator/info` - Application information
- `GET /actuator/metrics` - Metrics list
- `GET /actuator/metrics/{name}` - Specific metric
- `GET /actuator/prometheus` - Prometheus format metrics
info: app: name: ${spring.application.name} version: '@project.version@' java: version: ${java.version}

**常用Actuator端点**:
- `GET /actuator/health` - 应用健康状态
- `GET /actuator/health/liveness` - Kubernetes存活性探针
- `GET /actuator/health/readiness` - Kubernetes就绪性探针
- `GET /actuator/info` - 应用基础信息
- `GET /actuator/metrics` - 指标列表
- `GET /actuator/metrics/{name}` - 具体指标详情
- `GET /actuator/prometheus` - Prometheus格式的指标数据

Testing

测试

Unit Testing Controllers

控制器单元测试

java
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void shouldReturnUserById() throws Exception {
        UserDto user = UserDto.builder()
            .id(1L)
            .email("test@example.com")
            .name("Test User")
            .build();

        when(userService.findById(1L)).thenReturn(Optional.of(user));

        mockMvc.perform(get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }

    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound());
    }

    @Test
    void shouldCreateUser() throws Exception {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("new@example.com");
        request.setName("New User");
        request.setPassword("password123");

        UserDto created = UserDto.builder()
            .id(1L)
            .email("new@example.com")
            .name("New User")
            .build();

        when(userService.create(any())).thenReturn(created);

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("new@example.com"));
    }
}
java
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void shouldReturnUserById() throws Exception {
        UserDto user = UserDto.builder()
            .id(1L)
            .email("test@example.com")
            .name("测试用户")
            .build();

        when(userService.findById(1L)).thenReturn(Optional.of(user));

        mockMvc.perform(get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }

    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound());
    }

    @Test
    void shouldCreateUser() throws Exception {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("new@example.com");
        request.setName("新用户");
        request.setPassword("password123");

        UserDto created = UserDto.builder()
            .id(1L)
            .email("new@example.com")
            .name("新用户")
            .build();

        when(userService.create(any())).thenReturn(created);

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("new@example.com"));
    }
}

Integration Testing

集成测试

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateAndRetrieveUser() {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("integration@test.com");
        request.setName("Integration Test");
        request.setPassword("password123");

        ResponseEntity<UserDto> createResponse = restTemplate.postForEntity(
            "/api/v1/users", request, UserDto.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        assertThat(createResponse.getBody().getEmail()).isEqualTo("integration@test.com");

        Long userId = createResponse.getBody().getId();
        ResponseEntity<UserDto> getResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, UserDto.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test");
    }
}
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateAndRetrieveUser() {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("integration@test.com");
        request.setName("集成测试用户");
        request.setPassword("password123");

        ResponseEntity<UserDto> createResponse = restTemplate.postForEntity(
            "/api/v1/users", request, UserDto.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        assertThat(createResponse.getBody().getEmail()).isEqualTo("integration@test.com");

        Long userId = createResponse.getBody().getId();
        ResponseEntity<UserDto> getResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, UserDto.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getName()).isEqualTo("集成测试用户");
    }
}

Repository Testing

Repository测试

java
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByEmail() {
        User user = User.builder()
            .email("test@example.com")
            .name("Test")
            .passwordHash("hash")
            .build();
        entityManager.persistAndFlush(user);

        Optional<User> found = userRepository.findByEmail("test@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Test");
    }
}
java
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByEmail() {
        User user = User.builder()
            .email("test@example.com")
            .name("测试")
            .passwordHash("hash")
            .build();
        entityManager.persistAndFlush(user);

        Optional<User> found = userRepository.findByEmail("test@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("测试");
    }
}

Best Practices

最佳实践

1. Use Constructor Injection

1. 使用构造函数注入

java
// Prefer constructor injection with final fields
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;  // final = immutable
}
java
// 优先使用final字段的构造函数注入
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;  // final = 不可变
}

2. Layer Separation

2. 层级分离

java
// Controller -> Service -> Repository
// DTOs for API layer, Entities for persistence layer
// Never expose entities directly in REST responses
java
// 控制器 -> 服务层 -> 数据访问层
// API层使用DTO,持久化层使用实体
// 永远不要在REST响应中直接返回实体

3. Transaction Management

3. 事务管理

java
@Service
@Transactional(readOnly = true)  // Default read-only
public class UserService {

    @Transactional  // Write transaction
    public void updateUser() { }
}
java
@Service
@Transactional(readOnly = true)  // 默认只读
public class UserService {

    @Transactional  // 写操作单独加事务
    public void updateUser() { }
}

4. Configuration Externalization

4. 配置外置

yaml
undefined
yaml
undefined

Use environment variables for secrets

敏感信息使用环境变量注入

spring: datasource: password: ${DB_PASSWORD} # From environment
undefined
spring: datasource: password: ${DB_PASSWORD} # 从环境变量读取
undefined

5. Error Handling

5. 错误处理

java
// Use @RestControllerAdvice for global exception handling
// Return consistent error responses
// Never expose internal details in production
java
// 使用@RestControllerAdvice做全局异常处理
// 返回统一格式的错误响应
// 生产环境永远不要暴露内部错误细节

Resources

参考资源

Related Skills

相关技能

When using Spring Boot, consider these complementary skills:
  • mongodb: NoSQL database integration with Spring Data MongoDB
  • docker: Containerizing Spring Boot applications
  • kubernetes: Deploying Spring Boot microservices
  • postgresql: Relational database patterns with JPA
使用Spring Boot时,可搭配以下相关技能使用:
  • mongodb: 基于Spring Data MongoDB集成NoSQL数据库
  • docker: 容器化Spring Boot应用
  • kubernetes: 部署Spring Boot微服务
  • postgresql: 结合JPA使用关系型数据库模式