spring-boot-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpring Boot Expert
Spring Boot 专家指南
Expert guidance for Spring Boot development, Spring Framework, building REST APIs, and microservices architecture.
为Spring Boot开发、Spring Framework、REST API构建及微服务架构提供专家级指导。
Core Concepts
核心概念
Spring Boot Fundamentals
Spring Boot 基础
- Auto-configuration
- Dependency injection
- Spring Boot Starters
- Application properties
- Profiles and configuration
- Spring Boot Actuator
- Auto-configuration
- Dependency injection
- Spring Boot Starters
- Application properties
- Profiles与配置
- Spring Boot Actuator
Spring Framework
Spring Framework 核心
- Spring Core (IoC, DI)
- Spring Data JPA
- Spring Security
- Spring Web MVC
- Spring AOP
- Spring Transaction Management
- Spring Core (IoC, DI)
- Spring Data JPA
- Spring Security
- Spring Web MVC
- Spring AOP
- Spring 事务管理
Microservices
微服务
- Service discovery
- API Gateway
- Circuit breakers
- Distributed tracing
- Configuration management
- Service discovery
- API Gateway
- Circuit breakers
- Distributed tracing
- Configuration management
Spring Boot Application
Spring Boot 应用
java
// Main application class
@SpringBootApplication
@EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Entity with JPA
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Post> posts = new ArrayList<>();
// Getters and setters
}
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author;
@CreatedDate
private LocalDateTime createdAt;
// Getters and setters
}java
// Main application class
@SpringBootApplication
@EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Entity with JPA
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Post> posts = new ArrayList<>();
// Getters and setters
}
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author;
@CreatedDate
private LocalDateTime createdAt;
// Getters and setters
}REST API Controller
REST API 控制器
java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDto>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction direction
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
Page<UserDto> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDto> createUser(
@Valid @RequestBody UserCreateDto userDto
) {
UserDto created = userService.create(userDto);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateDto userDto
) {
return userService.update(id, userDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}
// DTOs with validation
public record UserDto(
Long id,
String email,
LocalDateTime createdAt
) {}
public record UserCreateDto(
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password
) {}
public record UserUpdateDto(
@Email String email
) {}java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDto>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction direction
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
Page<UserDto> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDto> createUser(
@Valid @RequestBody UserCreateDto userDto
) {
UserDto created = userService.create(userDto);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateDto userDto
) {
return userService.update(id, userDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}
// DTOs with validation
public record UserDto(
Long id,
String email,
LocalDateTime createdAt
) {}
public record UserCreateDto(
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password
) {}
public record UserUpdateDto(
@Email String email
) {}Service Layer
服务层
java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public Page<UserDto> findAll(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toDto);
}
public Optional<UserDto> findById(Long id) {
return userRepository.findById(id)
.map(userMapper::toDto);
}
public Optional<UserDto> findByEmail(String email) {
return userRepository.findByEmail(email)
.map(userMapper::toDto);
}
@Transactional
public UserDto create(UserCreateDto dto) {
if (userRepository.existsByEmail(dto.email())) {
throw new DuplicateEmailException("Email already exists");
}
User user = new User();
user.setEmail(dto.email());
user.setPassword(passwordEncoder.encode(dto.password()));
User saved = userRepository.save(user);
return userMapper.toDto(saved);
}
@Transactional
public Optional<UserDto> update(Long id, UserUpdateDto dto) {
return userRepository.findById(id)
.map(user -> {
if (dto.email() != null) {
user.setEmail(dto.email());
}
return userMapper.toDto(user);
});
}
@Transactional
public boolean delete(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
}
// Mapper with MapStruct
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserCreateDto dto);
}java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public Page<UserDto> findAll(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toDto);
}
public Optional<UserDto> findById(Long id) {
return userRepository.findById(id)
.map(userMapper::toDto);
}
public Optional<UserDto> findByEmail(String email) {
return userRepository.findByEmail(email)
.map(userMapper::toDto);
}
@Transactional
public UserDto create(UserCreateDto dto) {
if (userRepository.existsByEmail(dto.email())) {
throw new DuplicateEmailException("Email already exists");
}
User user = new User();
user.setEmail(dto.email());
user.setPassword(passwordEncoder.encode(dto.password()));
User saved = userRepository.save(user);
return userMapper.toDto(saved);
}
@Transactional
public Optional<UserDto> update(Long id, UserUpdateDto dto) {
return userRepository.findById(id)
.map(user -> {
if (dto.email() != null) {
user.setEmail(dto.email());
}
return userMapper.toDto(user);
});
}
@Transactional
public boolean delete(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
}
// Mapper with MapStruct
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserCreateDto dto);
}Spring Security with JWT
基于JWT的Spring Security
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@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);
}
}
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public String generateToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, jwtExpiration);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@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);
}
}
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public String generateToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, jwtExpiration);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}Exception Handling
异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"NOT_FOUND",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicateEmail(DuplicateEmailException ex) {
ErrorResponse error = new ErrorResponse(
"DUPLICATE_EMAIL",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException ex
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ValidationErrorResponse response = new ValidationErrorResponse(
"VALIDATION_ERROR",
"Request validation failed",
errors,
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
public record ErrorResponse(
String code,
String message,
LocalDateTime timestamp
) {}
public record ValidationErrorResponse(
String code,
String message,
Map<String, String> errors,
LocalDateTime timestamp
) {}java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"NOT_FOUND",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicateEmail(DuplicateEmailException ex) {
ErrorResponse error = new ErrorResponse(
"DUPLICATE_EMAIL",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException ex
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ValidationErrorResponse response = new ValidationErrorResponse(
"VALIDATION_ERROR",
"Request validation failed",
errors,
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
public record ErrorResponse(
String code,
String message,
LocalDateTime timestamp
) {}
public record ValidationErrorResponse(
String code,
String message,
Map<String, String> errors,
LocalDateTime timestamp
) {}Configuration
配置
yaml
undefinedyaml
undefinedapplication.yml
application.yml
spring:
application:
name: user-service
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
server:
port: 8080
error:
include-message: always
include-binding-errors: always
jwt:
secret: ${JWT_SECRET:your-secret-key-here}
expiration: 3600000 # 1 hour
logging:
level:
root: INFO
com.example: DEBUG
undefinedspring:
application:
name: user-service
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
server:
port: 8080
error:
include-message: always
include-binding-errors: always
jwt:
secret: ${JWT_SECRET:your-secret-key-here}
expiration: 3600000 # 1 hour
logging:
level:
root: INFO
com.example: DEBUG
undefinedTesting
测试
java
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldCreateUser() throws Exception {
UserCreateDto dto = new UserCreateDto("test@example.com", "password123");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
@Test
void shouldGetUser() throws Exception {
User user = createTestUser();
mockMvc.perform(get("/api/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(user.getEmail()));
}
@Test
void shouldReturnNotFoundForInvalidId() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
private User createTestUser() {
User user = new User();
user.setEmail("test@example.com");
user.setPassword("hashed-password");
return userRepository.save(user);
}
}
// Service unit test
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUser() {
UserCreateDto dto = new UserCreateDto("test@example.com", "password123");
User user = new User();
UserDto expected = new UserDto(1L, "test@example.com", LocalDateTime.now());
when(userRepository.existsByEmail(dto.email())).thenReturn(false);
when(passwordEncoder.encode(dto.password())).thenReturn("hashed");
when(userRepository.save(any(User.class))).thenReturn(user);
when(userMapper.toDto(user)).thenReturn(expected);
UserDto result = userService.create(dto);
assertNotNull(result);
assertEquals(expected.email(), result.email());
verify(userRepository).save(any(User.class));
}
}java
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldCreateUser() throws Exception {
UserCreateDto dto = new UserCreateDto("test@example.com", "password123");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
@Test
void shouldGetUser() throws Exception {
User user = createTestUser();
mockMvc.perform(get("/api/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(user.getEmail()));
}
@Test
void shouldReturnNotFoundForInvalidId() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
private User createTestUser() {
User user = new User();
user.setEmail("test@example.com");
user.setPassword("hashed-password");
return userRepository.save(user);
}
}
// Service unit test
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUser() {
UserCreateDto dto = new UserCreateDto("test@example.com", "password123");
User user = new User();
UserDto expected = new UserDto(1L, "test@example.com", LocalDateTime.now());
when(userRepository.existsByEmail(dto.email())).thenReturn(false);
when(passwordEncoder.encode(dto.password())).thenReturn("hashed");
when(userRepository.save(any(User.class))).thenReturn(user);
when(userMapper.toDto(user)).thenReturn(expected);
UserDto result = userService.create(dto);
assertNotNull(result);
assertEquals(expected.email(), result.email());
verify(userRepository).save(any(User.class));
}
}Best Practices
最佳实践
- Use constructor injection
- Separate concerns (Controller/Service/Repository)
- Implement proper exception handling
- Use DTOs for API layer
- Write comprehensive tests
- Use database migrations (Flyway/Liquibase)
- Implement security properly
- Use profiles for different environments
- Enable Spring Boot Actuator for monitoring
- Use connection pooling
- Implement caching where appropriate
- Follow RESTful conventions
- 使用构造函数注入
- 关注点分离(控制器/服务/仓库)
- 实现完善的异常处理
- 在API层使用DTO
- 编写全面的测试用例
- 使用数据库迁移工具(Flyway/Liquibase)
- 正确实现安全机制
- 为不同环境使用配置文件
- 启用Spring Boot Actuator进行监控
- 使用连接池
- 在合适的场景实现缓存
- 遵循RESTful规范
Anti-Patterns
反模式
❌ Field injection
❌ Business logic in controllers
❌ No exception handling
❌ Exposing entities directly
❌ Hardcoded configuration
❌ No transaction management
❌ Missing validation
❌ 字段注入
❌ 在控制器中编写业务逻辑
❌ 无异常处理机制
❌ 直接暴露实体类
❌ 硬编码配置参数
❌ 缺失事务管理
❌ 缺少参数校验
Resources
参考资源
- Spring Boot Documentation: https://spring.io/projects/spring-boot
- Spring Framework: https://spring.io/projects/spring-framework
- Spring Data JPA: https://spring.io/projects/spring-data-jpa
- Spring Security: https://spring.io/projects/spring-security
- Baeldung: https://www.baeldung.com/
- Spring Boot 官方文档: https://spring.io/projects/spring-boot
- Spring Framework 官方文档: https://spring.io/projects/spring-framework
- Spring Data JPA 官方文档: https://spring.io/projects/spring-data-jpa
- Spring Security 官方文档: https://spring.io/projects/spring-security
- Baeldung: https://www.baeldung.com/