关于 spring-boot
这项 Skill 介绍 Spring Boot 3.x 的开发模式,面向构建生产级 Java 应用的开发者,涵盖应用配置、Web 与数据层、安全、监控、测试及代码质量问题。
主要功能
- 01自动配置与嵌入式服务器
- 02REST 控制器与分层结构
- 03Spring Data JPA 仓库
- 04Spring Security 示例
- 05Actuator 健康状态与指标
- 06测试和质量规范
适用场景
- 01搭建 Java Web 应用
- 02构建带数据库访问的 REST API
- 03添加身份验证与授权
- 04测试控制器、仓库和集成
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill spring-boot安装后在 Claude Code 中输入/spring-boot确认已加载。
附带文件metadata.jsonreferences/quality-antipatterns.md
详细介绍
Spring Boot 3.x - 生产就绪的 Java 框架
概述
Spring Boot 是一款约定优于配置的 Java 框架,可通过最少的配置构建生产就绪的应用程序。它提供自动配置、嵌入式服务器,以及健康检查和指标等生产就绪功能。
主要功能:
- 自动配置(合理的默认设置)
- 嵌入式服务器(Tomcat、Jetty、Undertow)
- 使用 @Autowired 进行依赖注入
- 使用 Spring Data JPA 访问数据库
- 使用 Spring Security 进行身份验证/授权
- 使用 Actuator 进行生产环境监控
- 内置测试支持
要求:
- Java 17+(Spring Boot 3.x 要求最低使用 Java 17)
- Maven 或 Gradle
快速开始:
# Create project from 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
# Run the application
cd myapp
./mvnw spring-boot:run
项目结构
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
核心注解
应用程序设置
// Main application class
@SpringBootApplication // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class MyappApplication {
public static void main(String[] args) {
SpringApplication.run(MyappApplication.class, args);
}
}
依赖注入
// 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
}
组件构型注解
@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
REST 控制器
基本控制器
@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/[email protected]
@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));
}
}
请求/响应 DTO
// 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();
}
}
服务层
@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());
}
}
仓库层(Spring Data JPA)
基本仓库
@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);
}
实体类
@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<>();
}
配置
application.yml
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
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
特定配置文件的配置
# application-dev.yml
spring:
jpa:
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example.myapp: DEBUG
org.springframework.web: DEBUG
---
# application-prod.yml
spring:
jpa:
show-sql: false
properties:
hibernate:
generate_statistics: false
logging:
level:
com.example.myapp: INFO
org.springframework.web: WARN
配置属性类
@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();
}
}
异常处理
全局异常处理器
@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);
}
}
Spring Security
安全配置(Spring Security 6.x)
@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 过滤器
@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 端点
# 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
# Application info
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 格式的指标
测试
控制器单元测试
@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("[email protected]")
.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("[email protected]"));
}
@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("[email protected]");
request.setName("New User");
request.setPassword("password123");
UserDto created = UserDto.builder()
.id(1L)
.email("[email protected]")
.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("[email protected]"));
}
}
集成测试
@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("[email protected]");
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("[email protected]");
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");
}
}
仓库测试
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
User user = User.builder()
.email("[email protected]")
.name("Test")
.passwordHash("hash")
.build();
entityManager.persistAndFlush(user);
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Test");
}
}
最佳实践
1. 使用构造函数注入
// Prefer constructor injection with final fields
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // final = immutable
}
2. 分层隔离
// Controller -> Service -> Repository
// DTOs for API layer, Entities for persistence layer
// Never expose entities directly in REST responses
3. 事务管理
@Service
@Transactional(readOnly = true) // Default read-only
public class UserService {
@Transactional // Write transaction
public void updateUser() { }
}
4. 配置外部化
# Use environment variables for secrets
spring:
datasource:
password: ${DB_PASSWORD} # From environment
5. 错误处理
// Use @RestControllerAdvice for global exception handling
// Return consistent error responses
// Never expose internal details in production
代码质量与稳健性反模式
除了框架模式,还要留意服务、控制器和异常处理器中反复出现的稳健性与可变更性缺陷。静态分析可以发现这些问题,但手动编写代码时很容易引入:
- 方法中隐藏的通用
catch (Exception)— 捕获能够处理的特定类型。只有在顶层边界(例如@RestControllerAdvice的兜底处理)中,宽泛捕获才是正确的,并且应注释说明这是有意为之。 - 只重新抛出同一异常的 catch 子句 — 删除它们,或让它们补充上下文(包装成有类型的领域异常)。
- 抛出原始的
RuntimeException/Exception— 抛出有类型的领域异常,以便@ExceptionHandler将其映射到正确的 HTTP 状态码。 - 嵌套的
try/catch— 将内部逻辑提取到独立方法中。 - 没有最终
else的if … else if和 没有default的switch— 处理剩余情况,或说明为何无需处理(防御式编程)。 - 从非空 case 穿透的 switch 和 嵌套 switch — 优先使用箭头
switch(Java 14+)和枚举/密封类型,以便由编译器强制检查穷尽性。 - 可合并的嵌套
if和 在循环体内修改循环计数器 — 合并条件;绝不要在for循环体内重新赋值计数器。
请参阅 质量反模式,了解每种缺陷的合规/不合规 Java 示例、严重级别和误报过滤规则。相关模式源自 CAST Highlight 代码质量指标(https://doc.casthighlight.com/)。
资源
- Spring Boot 文档:https://docs.spring.io/spring-boot/docs/current/reference/html/
- Spring Data JPA:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
- Spring Security:https://docs.spring.io/spring-security/reference/
- Spring Initializr:https://start.spring.io/
- Baeldung 教程:https://www.baeldung.com/spring-boot
相关技能
使用 Spring Boot 时,可考虑搭配使用以下技能:
- mongodb:使用 Spring Data MongoDB 集成 NoSQL 数据库
- docker:将 Spring Boot 应用容器化
- kubernetes:部署 Spring Boot 微服务
- postgresql:使用 JPA 实现关系型数据库模式
评论
支持 Markdown