--- name: spring-boot-expert version: 1.0.0 description: Expert-level Spring Boot, Spring Framework, REST APIs, and microservices development category: frameworks tags: [spring-boot, java, spring-framework, rest-api, microservices] allowed-tools: - Read - Write - Edit - Bash(mvn:*, gradle:*, java:*) --- # Spring Boot Expert Expert guidance for Spring Boot development, Spring Framework, building REST APIs, and microservices architecture. ## Core Concepts ### Spring Boot Fundamentals - Auto-configuration - Dependency injection - Spring Boot Starters - Application properties - Profiles and configuration - Spring Boot Actuator ### Spring Framework - Spring Core (IoC, DI) - Spring Data JPA - Spring Security - Spring Web MVC - Spring AOP - Spring Transaction Management ### Microservices - Service discovery - API Gateway - Circuit breakers - Distributed tracing - Configuration management ## Spring Boot Application ```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 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 ```java @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping public ResponseEntity> 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 users = userService.findAll(pageable); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity getUser(@PathVariable Long id) { return userService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity 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 updateUser( @PathVariable Long id, @Valid @RequestBody UserUpdateDto userDto ) { return userService.update(id, userDto) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @DeleteMapping("/{id}") public ResponseEntity 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 findAll(Pageable pageable) { return userRepository.findAll(pageable) .map(userMapper::toDto); } public Optional findById(Long id) { return userRepository.findById(id) .map(userMapper::toDto); } public Optional 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 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 { Optional findByEmail(String email); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.createdAt > :date") List 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 ```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 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 extractClaim(String token, Function 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 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 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 handleValidation( MethodArgumentNotValidException ex ) { Map 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 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 errors, LocalDateTime timestamp ) {} ``` ## Configuration ```yaml # 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 ``` ## Testing ```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 ## 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/