Auto-configuration, Spring MVC, Data JPA, Security, REST APIs, testing, and microservices.
// ── Main Application ──
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// ── Core Annotations ──
@SpringBootApplication // @Configuration + @EnableAutoConfiguration + @ComponentScan
@Configuration // Marks class as a source of bean definitions
@Component // Auto-detected component
@Service // Business logic layer
@Repository // Data access layer (enables exception translation)
@Controller // Web controller
@RestController // @Controller + @ResponseBody (returns JSON)
@RequestMapping("/api") // Maps HTTP requests to handler methods
@Autowired // Dependency injection
@Qualifier("beanName") // Disambiguate bean injection
@Value("${app.name}") // Inject property value
@Bean // Creates a bean managed by Spring container
@Profile("dev") // Component only active in specific profile
@Primary // Primary bean when multiple candidates exist# ── application.yml ──
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: demo-app
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER:postgres}
password: ${DB_PASSWORD:secret}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
open-in-view: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
jackson:
serialization:
indent-output: true
write-dates-as-timestamps: false
default-property-inclusion: non_null
app:
name: My Application
jwt:
secret: ${JWT_SECRET:my-super-secret-key}
expiration: 86400000
logging:
level:
root: INFO
com.example.demo: DEBUG
org.springframework.security: DEBUG| Annotation | HTTP Method | Usage |
|---|---|---|
| @GetMapping("/users") | GET | Retrieve resource(s) |
| @PostMapping("/users") | POST | Create resource |
| @PutMapping("/users/{id}") | PUT | Full update |
| @PatchMapping("/users/{id}") | PATCH | Partial update |
| @DeleteMapping("/users/{id}") | DELETE | Delete resource |
| @RequestMapping(value="/users", method=RequestMethod.GET) | GET | Generic mapping |
| Annotation | Purpose | Example |
|---|---|---|
| @PathVariable | URI path variable | @PathVariable Long id |
| @RequestParam | Query parameter | @RequestParam String q |
| @RequestBody | JSON request body | @RequestBody UserDto dto |
| @RequestHeader | HTTP header | @RequestHeader("Auth") String token |
| @ResponseStatus | HTTP status code | @ResponseStatus(HttpStatus.CREATED) |
| @ExceptionHandler | Exception handler | Handles specific exceptions |
| @Valid | Trigger validation | Validates @RequestBody |
package com.example.demo.controller;
import com.example.demo.dto.*;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sort) {
return ResponseEntity.ok(userService.findAll(page, size, sort));
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
UserResponse created = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
@GetMapping("/search")
public ResponseEntity<Page<UserResponse>> searchUsers(
@RequestParam String query,
Pageable pageable) {
return ResponseEntity.ok(userService.search(query, pageable));
}
}// ── Data Transfer Objects (DTOs) ──
package com.example.demo.dto;
import jakarta.validation.constraints.*;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class CreateUserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, max = 128)
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).*$",
message = "Password must contain letter, number, uppercase")
private String password;
@Size(max = 200)
private String bio;
}
@Data
@Builder
public class UserResponse {
private Long id;
private String name;
private String email;
private String bio;
private String avatarUrl;
private boolean active;
private java.time.LocalDateTime createdAt;
}
// ── Global Exception Handler ──
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_ERROR", message));
}
}@ResponseStatus for void methods that need specific status codes.package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = "email")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(nullable = false)
private String password;
@Column(length = 500)
private String bio;
private String avatarUrl;
@Enumerated(EnumType.STRING)
private UserRole role = UserRole.USER;
private boolean active = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("createdAt DESC")
private List<Post> posts = new ArrayList<>();
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
public enum UserRole {
USER, ADMIN, MODERATOR
}package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// Derived query methods
Optional<User> findByEmail(String email);
Optional<User> findByEmailAndActiveTrue(String email);
List<User> findByNameContainingIgnoreCase(String name);
boolean existsByEmail(String email);
long countByActiveTrue();
// Sorting and pagination
Page<User> findByActiveTrue(Pageable pageable);
List<User> findByRole(UserRole role, Sort sort);
// Custom @Query (JPQL)
@Query("SELECT u FROM User u WHERE u.active = true " +
"AND (u.name LIKE %:query% OR u.email LIKE %:query%)")
Page<User> searchUsers(@Param("query") String query, Pageable pageable);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE created_at > :date",
nativeQuery = true)
List<User> findRecentUsers(@Param("date") LocalDateTime date);
// Update query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
// Delete query
@Modifying
@Query("DELETE FROM User u WHERE u.active = false AND u.createdAt < :date")
int deleteOldInactiveUsers(@Param("date") LocalDateTime date);
}| Annotation | Type | Fetch Type |
|---|---|---|
| @OneToMany | One-to-many | LAZY (default) |
| @ManyToOne | Many-to-one | EAGER (default) |
| @ManyToMany | Many-to-many | LAZY (default) |
| @OneToOne | One-to-one | EAGER (default) |
| Cascade Type | Description |
|---|---|
| PERSIST | Cascade save() calls |
| MERGE | Cascade merge() calls |
| REMOVE | Cascade delete() calls |
| REFRESH | Cascade refresh() calls |
| DETACH | Cascade detach() calls |
| ALL | All cascade operations |
package com.example.demo.config;
import com.example.demo.security.*;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.*;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.configuration.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.*;
import org.springframework.security.crypto.password.*;
import org.springframework.security.web.*;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}package com.example.demo.security;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserService userService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null &&
SecurityContextHolder.getContext().getAuthentication() == null) {
var userDetails = userService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
var authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}STATELESS) for REST APIs. Disable CSRF for APIs (JWT handles this). Validate JWT tokens on every request and use short expiration times with refresh tokens.package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// ── Log all controller methods ──
@Around("execution(* com.example.demo.controller..*(..))")
public Object logControllerMethods(ProceedingJoinPoint joinPoint)
throws Throwable {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
log.info("Entering: {} with args: {}", methodName, args);
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("Exiting: {} in {} ms", methodName, duration);
return result;
}
// ── Log specific annotation ──
@AfterReturning(pointcut = "@annotation(auditable)",
returning = "result")
public void logAudit(Auditable auditable, Object result) {
log.info("Audit action '{}' returned: {}",
auditable.action(), result);
}
// ── Exception handling ──
@AfterThrowing(pointcut = "execution(* com.example.demo.service..*(..))",
throwing = "exception")
public void logServiceException(Exception exception) {
log.error("Service exception: {}", exception.getMessage(), exception);
}
}package com.example.demo.service;
import com.example.demo.dto.*;
import com.example.demo.model.*;
import com.example.demo.repository.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.*;
import java.util.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = User.builder()
.id(1L).name("John Doe").email("john@example.com")
.password("hashed_password").build();
}
@Test
void findById_shouldReturnUser() {
given(userRepository.findById(1L)).willReturn(Optional.of(testUser));
UserResponse response = userService.findById(1L);
assertThat(response.getName()).isEqualTo("John Doe");
then(userRepository).should().findById(1L);
}
@Test
void findById_shouldThrowWhenNotFound() {
given(userRepository.findById(99L)).willReturn(Optional.empty());
assertThatThrownBy(() -> userService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessage("User not found with id: 99");
}
@Test
void create_shouldPersistUser() {
CreateUserRequest request = CreateUserRequest.builder()
.name("Jane Doe").email("jane@example.com")
.password("Password123").build();
given(userRepository.existsByEmail("jane@example.com")).willReturn(false);
given(userRepository.save(any(User.class)))
.willAnswer(inv -> {
User u = inv.getArgument(0);
u.setId(2L);
return u;
});
UserResponse response = userService.create(request);
assertThat(response.getId()).isEqualTo(2L);
then(userRepository).should().save(any(User.class));
}
@Test
void findAll_shouldReturnPagedUsers() {
Page<User> page = new PageImpl<>(List.of(testUser));
given(userRepository.findByActiveTrue(any(Pageable.class))).willReturn(page);
Page<UserResponse> result = userService.findAll(0, 20, "name");
assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0).getName()).isEqualTo("John Doe");
}
}@WebMvcTest for controller tests (no Spring context),@DataJpaTest for repository tests (in-memory DB),@SpringBootTest for full integration tests. Use Mockito for unit tests with given/when/then (BDDMockito) pattern.All are @Component stereotypes for auto-detection via component scanning.@Service indicates business logic (intent, no special behavior).@Repositorycatches platform-specific exceptions and re-throws as Spring'sDataAccessException.@Controller indicates a web controller (routes requests to views).@RestController = @Controller + @ResponseBody.
@EnableAutoConfiguration (part of @SpringBootApplication) automatically configures beans based on classpath dependencies. It uses spring.factories (or org.springframework.boot.autoconfigure.AutoConfiguration.importsin Spring Boot 3) to find auto-configuration classes. Conditional annotations like@ConditionalOnClass, @ConditionalOnMissingBean, and@ConditionalOnProperty control when configurations apply.
JPA (Java Persistence API) is a specification/interface for ORM in Java. Hibernate is the most popular JPA implementation. JPA provides annotations like @Entity, @Id, and the EntityManager interface. Hibernate provides the actual implementation, plus additional features like HQL, caching (first-level and second-level), and advanced mappings. Spring Data JPA builds on top of JPA to provide repository abstractions.