Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:30 +08:00
commit 171acedaa4
220 changed files with 85967 additions and 0 deletions

View File

@@ -0,0 +1,761 @@
# Architecture Patterns for REST APIs
## Layered Architecture
### Feature-Based Structure
```
feature-name/
├── domain/
│ ├── model/ # Domain entities (Spring-free)
│ │ └── User.java
│ ├── repository/ # Domain ports (interfaces)
│ │ └── UserRepository.java
│ └── service/ # Domain services
│ └── UserService.java
├── application/
│ ├── service/ # Use cases (@Service beans)
│ │ └── UserApplicationService.java
│ └── dto/ # Immutable DTOs/records
│ ├── UserRequest.java
│ └── UserResponse.java
├── presentation/
│ └── rest/ # Controllers and mappers
│ ├── UserController.java
│ ├── UserMapper.java
│ └── UserExceptionHandler.java
└── infrastructure/
└── persistence/ # JPA adapters
└── JpaUserRepository.java
```
### Domain Layer (Clean Architecture)
#### Domain Entity
```java
package com.example.domain.model;
import java.time.LocalDateTime;
import java.util.Objects;
public class User {
private final UserId id;
private final String name;
private final Email email;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
private User(UserId id, String name, Email email) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.email = Objects.requireNonNull(email);
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public static User create(UserId id, String name, Email email) {
return new User(id, name, email);
}
public User updateName(String newName) {
return new User(this.id, newName, this.email);
}
public User updateEmail(Email newEmail) {
return new User(this.id, this.name, newEmail);
}
// Getters
public UserId getId() { return id; }
public String getName() { return name; }
public Email getEmail() { return email; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
// Value objects
public class UserId {
private final Long value;
public UserId(Long value) {
this.value = Objects.requireNonNull(value);
if (value <= 0) {
throw new IllegalArgumentException("User ID must be positive");
}
}
public Long getValue() { return value; }
}
public class Email {
private final String value;
public Email(String value) {
this.value = Objects.requireNonNull(value);
if (!isValid(value)) {
throw new IllegalArgumentException("Invalid email format");
}
}
private boolean isValid(String email) {
return email.contains("@") && email.length() > 5;
}
public String getValue() { return value; }
}
```
#### Domain Repository Port
```java
package com.example.domain.repository;
import com.example.domain.model.User;
import com.example.domain.model.UserId;
import java.util.Optional;
public interface UserRepository {
Optional<User> findById(UserId id);
void save(User user);
void delete(UserId id);
boolean existsByEmail(Email email);
}
```
#### Domain Service
```java
package com.example.domain.service;
import com.example.domain.model.User;
import com.example.domain.model.Email;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UserDomainService {
private final UserRepository userRepository;
public User registerUser(String name, String email) {
Email emailObj = new Email(email);
if (userRepository.existsByEmail(emailObj)) {
throw new BusinessException("Email already exists");
}
User user = User.create(UserId.generate(), name, emailObj);
userRepository.save(user);
return user;
}
}
```
### Application Layer (Use Cases)
#### Application Service
```java
package com.example.application.service;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import com.example.application.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserApplicationService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Transactional
public UserResponse createUser(UserRequest request) {
log.info("Creating user: {}", request.getName());
User user = userMapper.toDomain(request);
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
@Transactional(readOnly = true)
public Page<UserResponse> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toResponse);
}
@Transactional(readOnly = true)
public UserResponse findUserById(Long id) {
return userRepository.findById(new UserId(id))
.map(userMapper::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
@Transactional
public UserResponse updateUser(Long id, UserRequest request) {
User user = userRepository.findById(new UserId(id))
.orElseThrow(() -> new EntityNotFoundException("User not found"));
User updated = user.updateName(request.getName());
User saved = userRepository.save(updated);
return userMapper.toResponse(saved);
}
@Transactional
public void deleteUser(Long id) {
userRepository.delete(new UserId(id));
}
}
```
#### DTOs
```java
package com.example.application.dto;
import lombok.Value;
import jakarta.validation.constraints.*;
@Value
public class UserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Valid email required")
private String email;
}
@Value
public class UserResponse {
Long id;
String name;
String email;
LocalDateTime createdAt;
LocalDateTime updatedAt;
}
```
### Presentation Layer (REST API)
#### Controller
```java
package com.example.presentation.rest;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.application.service.UserApplicationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class UserController {
private final UserApplicationService userApplicationService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserRequest request) {
UserResponse created = userApplicationService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(Pageable pageable) {
Page<UserResponse> users = userApplicationService.findAllUsers(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
UserResponse user = userApplicationService.findUserById(id);
return ResponseEntity.ok(user);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserRequest request) {
UserResponse updated = userApplicationService.updateUser(id, request);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userApplicationService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
```
#### Mapper
```java
package com.example.application.mapper;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.model.Email;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "id", source = "id.value")
UserResponse toResponse(User user);
User toDomain(UserRequest request);
default Email toEmail(String email) {
return new Email(email);
}
default String toString(Email email) {
return email.getValue();
}
}
```
### Infrastructure Layer (Adapters)
#### JPA Repository Adapter
```java
package com.example.infrastructure.persistence;
import com.example.domain.model.User;
import com.example.domain.model.UserId;
import com.example.domain.repository.UserRepository;
import com.example.infrastructure.entity.UserEntity;
import com.example.infrastructure.mapper.UserEntityMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class JpaUserRepository implements UserRepository {
private final SpringDataUserRepository springDataRepository;
private final UserEntityMapper entityMapper;
@Override
public Optional<User> findById(UserId id) {
return springDataRepository.findById(id.getValue())
.map(entityMapper::toDomain);
}
@Override
public void save(User user) {
UserEntity entity = entityMapper.toEntity(user);
springDataRepository.save(entity);
}
@Override
public void delete(UserId id) {
springDataRepository.deleteById(id.getValue());
}
@Override
public boolean existsByEmail(Email email) {
return springDataRepository.existsByEmail(email.getValue());
}
}
```
#### Spring Data Repository
```java
package com.example.infrastructure.persistence;
import com.example.infrastructure.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface SpringDataUserRepository extends JpaRepository<UserEntity, Long> {
boolean existsByEmail(String email);
@Query("SELECT u FROM UserEntity u WHERE u.email = :email")
Optional<UserEntity> findByEmail(@Param("email") String email);
}
```
## CQRS Pattern (Command Query Responsibility Segregation)
### Commands
```java
package com.example.application.command;
import lombok.Value;
import jakarta.validation.constraints.*;
@Value
public class CreateUserCommand {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
@NotBlank(message = "Email is required")
@Email
private String email;
}
@Value
public class UpdateUserCommand {
@NotNull(message = "User ID is required")
private Long userId;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
}
```
### Command Handlers
```java
package com.example.application.command.handler;
import com.example.application.command.CreateUserCommand;
import com.example.application.command.UpdateUserCommand;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserCommandHandler {
private final UserRepository userRepository;
@Transactional
public User handle(CreateUserCommand command) {
User user = User.create(UserId.generate(), command.getName(), new Email(command.getEmail()));
return userRepository.save(user);
}
@Transactional
public User handle(UpdateUserCommand command) {
User user = userRepository.findById(new UserId(command.getUserId()))
.orElseThrow(() -> new EntityNotFoundException("User not found"));
User updated = user.updateName(command.getName());
return userRepository.save(updated);
}
}
```
### Queries
```java
package com.example.application.query;
import lombok.Value;
import java.util.List;
@Value
public class FindAllUsersQuery {
int page;
int size;
String sortBy;
String sortDirection;
}
@Value
public class FindUserByIdQuery {
Long userId;
}
```
### Query Handlers
```java
package com.example.application.query.handler;
import com.example.application.query.FindAllUsersQuery;
import com.example.application.query.FindUserByIdQuery;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserQueryHandler {
private final UserRepository userRepository;
public Page<UserResponse> handle(FindAllUsersQuery query) {
Pageable pageable = PageRequest.of(
query.getPage(),
query.getSize(),
Sort.by(Sort.Direction.fromString(query.getSortDirection()), query.getSortBy())
);
return userRepository.findAll(pageable)
.map(this::toResponse);
}
public UserResponse handle(FindUserByIdQuery query) {
return userRepository.findById(new UserId(query.getUserId()))
.map(this::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId().getValue(),
user.getName(),
user.getEmail().getValue(),
user.getCreatedAt(),
user.getUpdatedAt()
);
}
}
```
## Event-Driven Architecture
### Domain Events
```java
package com.example.domain.event;
import lombok.Value;
import java.time.LocalDateTime;
@Value
public class UserCreatedEvent {
String userId;
String email;
LocalDateTime timestamp;
}
@Value
public class UserUpdatedEvent {
String userId;
String oldName;
String newName;
LocalDateTime timestamp;
}
```
### Event Publisher
```java
package com.example.domain.service;
import com.example.domain.event.UserCreatedEvent;
import com.example.domain.event.UserUpdatedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public void publishUserCreated(String userId, String email) {
UserCreatedEvent event = new UserCreatedEvent(userId, email, LocalDateTime.now());
eventPublisher.publishEvent(event);
log.info("Published UserCreatedEvent for user: {}", userId);
}
public void publishUserUpdated(String userId, String oldName, String newName) {
UserUpdatedEvent event = new UserUpdatedEvent(userId, oldName, newName, LocalDateTime.now());
eventPublisher.publishEvent(event);
log.info("Published UserUpdatedEvent for user: {}", userId);
}
}
```
### Event Listeners
```java
package com.example.application.event.listener;
import com.example.domain.event.UserCreatedEvent;
import com.example.domain.event.UserUpdatedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class UserEventListener {
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
log.info("User created: {} with email: {}", event.getUserId(), event.getEmail());
// Send welcome email, update analytics, etc.
}
@EventListener
public void handleUserUpdated(UserUpdatedEvent event) {
log.info("User {} updated: {} -> {}", event.getUserId(), event.getOldName(), event.getNewName());
// Update search index, send notification, etc.
}
}
```
## Microservices Architecture
### API Gateway Pattern
```java
package com.example.gateway;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://user-service"))
.route("order-service", r -> r.path("/api/orders/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service"))
.build();
}
}
```
### Service Communication
```java
package com.example.application.client;
import com.example.application.dto.UserResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "user-service", url = "${user.service.url}")
public interface UserServiceClient {
@GetMapping("/api/users/{id}")
UserResponse getUserById(@PathVariable Long id);
@GetMapping("/api/users")
List<UserResponse> getAllUsers();
}
```
## Testing Strategy
### Unit Tests
```java
package com.example.application.service;
import com.example.application.dto.UserRequest;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserApplicationServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserApplicationService userApplicationService;
@Test
void createUserShouldCreateUser() {
// Arrange
UserRequest request = new UserRequest("John Doe", "john@example.com");
User user = User.create(UserId.generate(), "John Doe", new Email("john@example.com"));
when(userRepository.save(any(User.class))).thenReturn(user);
// Act
UserResponse result = userApplicationService.createUser(request);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository).save(any(User.class));
}
}
```
### Integration Tests
```java
package com.example.presentation.rest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void createUserShouldReturnCreatedStatus() throws Exception {
// Arrange
String requestBody = """
{
"name": "John Doe",
"email": "john@example.com"
}
""";
// Act & Assert
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
}
```

View File

@@ -0,0 +1,597 @@
# Spring Boot REST API Examples
## Complete CRUD REST API with Validation
### Entity with Validation
```java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Valid email required")
@Column(unique = true)
private String email;
@Min(value = 18, message = "Must be at least 18")
@Max(value = 120, message = "Invalid age")
private Integer age;
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
private String password;
@Column(name = "is_active")
private Boolean active = true;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
```
### Service with Transaction Management
```java
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Transactional(readOnly = true)
public Page<UserResponse> findAll(Pageable pageable) {
log.debug("Fetching users page {} size {}", pageable.getPageNumber(), pageable.getPageSize());
return userRepository.findAll(pageable)
.map(this::toResponse);
}
@Transactional(readOnly = true)
public UserResponse findById(Long id) {
log.debug("Looking for user with id {}", id);
return userRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
@Transactional
public UserResponse create(CreateUserRequest request) {
log.info("Creating user with email: {}", request.getEmail());
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("Email already exists");
}
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
user.setPassword(passwordEncoder.encode(request.getPassword()));
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(saved.getEmail(), saved.getName());
return toResponse(saved);
}
@Transactional
public UserResponse update(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getEmail() != null) {
if (!user.getEmail().equals(request.getEmail()) &&
userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("Email already exists");
}
user.setEmail(request.getEmail());
}
if (request.getAge() != null) {
user.setAge(request.getAge());
}
User updated = userRepository.save(user);
return toResponse(updated);
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new EntityNotFoundException("User not found");
}
User user = userRepository.findById(id).orElseThrow();
emailService.sendDeletionEmail(user.getEmail(), user.getName());
userRepository.deleteById(id);
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAge(),
user.getActive(),
user.getCreatedAt(),
user.getUpdatedAt()
);
}
}
```
### Controller with Proper HTTP Methods
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<UserResponse> users = userService.findAll(pageable);
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", String.valueOf(users.getTotalElements()));
headers.add("X-Total-Pages", String.valueOf(users.getTotalPages()));
return ResponseEntity.ok()
.headers(headers)
.body(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@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)
.header("Location", "/api/users/" + created.getId())
.body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse updated = userService.update(id, request);
return ResponseEntity.ok(updated);
}
@PatchMapping("/{id}")
public ResponseEntity<UserResponse> patchUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse updated = userService.update(id, request);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
```
## API Versioning Examples
### URL Versioning
```java
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// Version 1 endpoints
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// Version 2 endpoints with different response format
}
```
### Header Versioning
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public ResponseEntity<UserResponse> getUsers(
@RequestHeader(value = "Accept-Version", defaultValue = "1.0") String version) {
if (version.equals("2.0")) {
return ResponseEntity.ok(v2UserResponse);
}
return ResponseEntity.ok(v1UserResponse);
}
}
```
### Media Type Versioning
```java
@GetMapping(produces = {
"application/vnd.company.v1+json",
"application/vnd.company.v2+json"
})
public ResponseEntity<UserResponse> getUsers(
@RequestHeader("Accept") String accept) {
if (accept.contains("v2")) {
return ResponseEntity.ok(v2UserResponse);
}
return ResponseEntity.ok(v1UserResponse);
}
```
## HATEOAS Implementation
### Response with Links
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponseWithLinks {
private Long id;
private String name;
private String email;
private Map<String, String> _links;
// Lombok generates constructors/getters/setters
}
@GetMapping("/{id}")
public ResponseEntity<UserResponseWithLinks> getUserWithLinks(@PathVariable Long id) {
UserResponse user = userService.findById(id);
Map<String, String> links = Map.of(
"self", "/api/users/" + id,
"all", "/api/users",
"update", "/api/users/" + id,
"delete", "/api/users/" + id
);
UserResponseWithLinks response = new UserResponseWithLinks(
user.getId(), user.getName(), user.getEmail(), links);
return ResponseEntity.ok(response);
}
```
### Advanced HATEOAS with Spring HATEOAS
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final EntityLinks entityLinks;
@GetMapping
public ResponseEntity<CollectionModel<UserResponse>> getAllUsers() {
List<UserResponse> users = userService.findAll().stream()
.map(this::toResponse)
.collect(Collectors.toList());
CollectionModel<UserResponse> resource = CollectionModel.of(users);
resource.add(entityLinks.linkToCollectionResource(UserController.class).withSelfRel());
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
return ResponseEntity.ok(resource);
}
@GetMapping("/{id}")
public ResponseEntity<EntityModel<UserResponse>> getUserById(@PathVariable Long id) {
UserResponse user = userService.findById(id);
EntityModel<UserResponse> resource = EntityModel.of(user);
resource.add(entityLinks.linkToItemResource(UserController.class, id).withSelfRel());
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
resource.add(linkTo(methodOn(UserController.class).getUserOrders(id)).withRel("orders"));
return ResponseEntity.ok(resource);
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getActive(),
user.getCreatedAt()
);
}
}
```
## Async Processing
### Asynchronous Controller
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class AsyncUserController {
private final AsyncUserService asyncUserService;
@GetMapping("/{id}")
public CompletableFuture<ResponseEntity<UserResponse>> getUserById(@PathVariable Long id) {
return asyncUserService.getUserById(id)
.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}
@PostMapping
public CompletableFuture<ResponseEntity<UserResponse>> createUser(
@Valid @RequestBody CreateUserRequest request) {
return asyncUserService.createUser(request)
.thenApply(created ->
ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.body(created))
.exceptionally(ex -> {
if (ex.getCause() instanceof BusinessException) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.internalServerError().build();
});
}
}
```
### Async Service Implementation
```java
@Service
@RequiredArgsConstructor
public class AsyncUserService {
private final UserService userService;
private final ExecutorService executor;
@Async
public CompletableFuture<UserResponse> getUserById(Long id) {
return CompletableFuture.supplyAsync(() -> userService.findById(id), executor);
}
@Async
public CompletableFuture<UserResponse> createUser(CreateUserRequest request) {
return CompletableFuture.supplyAsync(() -> userService.create(request), executor);
}
}
```
## File Upload and Download
### File Upload Controller
```java
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileController {
private final FileStorageService fileStorageService;
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("File is empty");
}
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/api/files/download/")
.path(fileName)
.toUriString();
FileUploadResponse response = new FileUploadResponse(
fileName, fileDownloadUri, file.getContentType(), file.getSize());
return ResponseEntity.ok(response);
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
Resource resource = fileStorageService.loadFileAsResource(fileName);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
```
### File Storage Service
```java
@Service
public class FileStorageService {
private final Path fileStorageLocation;
@Autowired
public FileStorageService(FileStorageProperties fileStorageProperties) {
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
.toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
}
}
public String storeFile(MultipartFile file) {
String fileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
try {
if (fileName.contains("..")) {
throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
}
Path targetLocation = this.fileStorageLocation.resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
return fileName;
} catch (IOException ex) {
throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
}
}
public Resource loadFileAsResource(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath);
if (resource.exists() && resource.isReadable()) {
return resource;
} else {
throw new FileNotFoundException("File not found " + fileName);
}
} catch (Exception ex) {
throw new FileNotFoundException("File not found " + fileName, ex);
}
}
}
```
## WebSocket Integration
### WebSocket Configuration
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
```
### WebSocket Controller
```java
@Controller
@RequiredArgsConstructor
public class WebSocketController {
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage;
}
@Scheduled(fixedRate = 5000)
public void sendPeriodicUpdates() {
messagingTemplate.convertAndSend("/topic/updates",
new UpdateMessage("System update", LocalDateTime.now()));
}
}
```
### Frontend Integration Example
```javascript
// JavaScript WebSocket client
class WebSocketClient {
constructor(url) {
this.url = url;
this.stompClient = null;
this.connected = false;
}
connect() {
const socket = new SockJS(this.url);
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, (frame) => {
this.connected = true;
console.log('Connected: ' + frame);
// Subscribe to topics
this.stompClient.subscribe('/topic/public', (message) => {
this.onMessage(message);
});
this.stompClient.subscribe('/topic/updates', (update) => {
this.onUpdate(update);
});
}, (error) => {
this.connected = false;
console.error('Error: ' + error);
});
}
sendMessage(message) {
if (this.connected) {
this.stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(message));
}
}
onMessage(message) {
const chatMessage = JSON.parse(message.body);
console.log('Received message:', chatMessage);
// Display message in UI
}
onUpdate(update) {
const updateMessage = JSON.parse(update.body);
console.log('Received update:', updateMessage);
// Update UI with system messages
}
}
```

View File

@@ -0,0 +1,88 @@
# HTTP Methods and Status Codes Reference
## HTTP Methods
| Method | Idempotent | Safe | Purpose | Typical Status |
|--------|-----------|------|---------|----------------|
| GET | Yes | Yes | Retrieve resource | 200, 304, 404 |
| POST | No | No | Create resource | 201, 400, 409 |
| PUT | Yes | No | Replace resource | 200, 204, 404 |
| PATCH | No | No | Partial update | 200, 204, 400 |
| DELETE | Yes | No | Remove resource | 204, 404 |
| HEAD | Yes | Yes | Like GET, no body | 200, 304, 404 |
| OPTIONS | Yes | Yes | Describe communication options | 200 |
### Idempotent Operations
An operation is idempotent if making the same request multiple times produces the same result as making it once.
### Safe Operations
A safe operation doesn't change the state of the server. Safe operations are always idempotent.
## HTTP Status Codes
### 2xx Success
- `200 OK`: Successful GET/PUT/PATCH
- `201 Created`: Successful POST (include Location header)
- `202 Accepted`: Async processing accepted
- `204 No Content`: Successful DELETE or POST with no content
- `206 Partial Content`: Range request successful
### 3xx Redirection
- `301 Moved Permanently`: Resource permanently moved
- `304 Not Modified`: Cache valid, use local copy
- `307 Temporary Redirect`: Temporary redirect
### 4xx Client Errors
- `400 Bad Request`: Invalid format or parameters
- `401 Unauthorized`: Authentication required
- `403 Forbidden`: Authenticated but not authorized
- `404 Not Found`: Resource doesn't exist
- `409 Conflict`: Constraint violation or conflict
- `422 Unprocessable Entity`: Validation failed (semantic error)
- `429 Too Many Requests`: Rate limit exceeded
### 5xx Server Errors
- `500 Internal Server Error`: Unexpected server error
- `502 Bad Gateway`: External service unavailable
- `503 Service Unavailable`: Server temporarily down
- `504 Gateway Timeout`: External service timeout
## Common REST API Patterns
### Resource URLs
```
GET /users # List all users
GET /users/123 # Get specific user
POST /users # Create user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
GET /users/123/orders # Get user's orders
```
### Query Parameters
```
GET /users?page=0&size=20&sort=createdAt,desc
- page: Page number (0-based)
- size: Number of items per page
- sort: Sorting format (field,direction)
```
### Response Headers
```
Location: /api/users/123 # For 201 Created responses
X-Total-Count: 45 # Total items count
Cache-Control: no-cache # Cache control
Content-Type: application/json # Response format
```
## Error Response Format
```json
{
"status": 400,
"error": "Bad Request",
"message": "Validation failed: name: Name cannot be blank, email: Valid email required",
"path": "/api/users",
"timestamp": "2024-01-15T10:30:00Z"
}
```

View File

@@ -0,0 +1,387 @@
# Pagination and Filtering Reference
## Pagination with Spring Data
### Basic Pagination
```java
@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 = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
}
```
### Pagination with Sorting
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
```
### Multi-field Sorting
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "name,createdAt") String sortFields) {
List<Sort.Order> orders = Arrays.stream(sortFields.split(","))
.map(field -> {
String direction = field.startsWith("-") ? "DESC" : "ASC";
String property = field.startsWith("-") ? field.substring(1) : field;
return new Sort.Order(Sort.Direction.fromString(direction), property);
})
.collect(Collectors.toList());
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
```
## Response Format
### Standard Page Response
```json
{
"content": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"pageable": {
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"pageNumber": 0,
"pageSize": 10,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 5,
"totalElements": 45,
"size": 10,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"first": true,
"numberOfElements": 10,
"empty": false
}
```
### Custom Page Response Wrapper
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private PageMetadata metadata;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageMetadata {
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
}
// Controller
@GetMapping("/users")
public ResponseEntity<PageResponse<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<User> pageResult = userService.findAll(pageable);
PageMetadata metadata = new PageMetadata(
page,
size,
pageResult.getTotalElements(),
pageResult.getTotalPages(),
pageResult.isFirst(),
pageResult.isLast()
);
PageResponse<UserResponse> response = new PageResponse<>(
pageResult.stream()
.map(this::toResponse)
.collect(Collectors.toList()),
metadata
);
return ResponseEntity.ok(response);
}
```
## Filtering
### Query Parameter Filtering
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Specification<User> spec = Specification.where(null);
if (name != null && !name.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"));
}
if (email != null && !email.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%"));
}
Pageable pageable = PageRequest.of(page, size);
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
### Dynamic Specification Builder
```java
public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null :
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null :
cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%");
}
public static Specification<User> isActive(Boolean active) {
return (root, query, cb) ->
active == null ? null :
cb.equal(root.get("active"), active);
}
public static Specification<User> createdAfter(LocalDate date) {
return (root, query, cb) ->
date == null ? null :
cb.greaterThanOrEqualTo(root.get("createdAt"), date.atStartOfDay());
}
}
// Usage
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(required = false) Boolean active,
@RequestParam(required = false) LocalDate createdAfter,
Pageable pageable) {
Specification<User> spec = Specification.where(UserSpecifications.hasName(name))
.and(UserSpecifications.hasEmail(email))
.and(UserSpecifications.isActive(active))
.and(UserSpecifications.createdAfter(createdAfter));
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
### Date Range Filtering
```java
@GetMapping("/orders")
public ResponseEntity<Page<OrderResponse>> getOrders(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
Pageable pageable) {
Specification<Order> spec = Specification.where(null);
if (startDate != null) {
spec = spec.and((root, query, cb) ->
cb.greaterThanOrEqualTo(root.get("createdAt"), startDate.atStartOfDay()));
}
if (endDate != null) {
spec = spec.and((root, query, cb) ->
cb.lessThanOrEqualTo(root.get("createdAt"), endDate.atEndOfDay()));
}
Page<Order> pageResult = orderService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
## Advanced Filtering
### Filter DTO Pattern
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserFilter {
private String name;
private String email;
private Boolean active;
private LocalDate createdAfter;
private LocalDate createdBefore;
private List<Long> roleIds;
public Specification<User> toSpecification() {
Specification<User> spec = Specification.where(null);
if (name != null && !name.isEmpty()) {
spec = spec.and(hasName(name));
}
if (email != null && !email.isEmpty()) {
spec = spec.and(hasEmail(email));
}
if (active != null) {
spec = spec.and(isActive(active));
}
if (createdAfter != null) {
spec = spec.and(createdAfter(createdAfter));
}
if (createdBefore != null) {
spec = spec.and(createdBefore(createdBefore));
}
if (roleIds != null && !roleIds.isEmpty()) {
spec = spec.and(hasRoles(roleIds));
}
return spec;
}
}
// Controller
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
UserFilter filter,
Pageable pageable) {
Specification<User> spec = filter.toSpecification();
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
## Link Headers for Pagination
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Pageable pageable) {
Page<UserResponse> pageResult = userService.findAll(pageable);
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", String.valueOf(pageResult.getTotalElements()));
headers.add("X-Total-Pages", String.valueOf(pageResult.getTotalPages()));
headers.add("X-Page-Number", String.valueOf(pageResult.getNumber()));
headers.add("X-Page-Size", String.valueOf(pageResult.getSize()));
// Link headers for pagination
if (pageResult.hasNext()) {
headers.add("Link", buildLinkHeader(pageResult.getNumber() + 1, size));
}
if (pageResult.hasPrevious()) {
headers.add("Link", buildLinkHeader(pageResult.getNumber() - 1, size));
}
return ResponseEntity.ok()
.headers(headers)
.body(pageResult);
}
private String buildLinkHeader(int page, int size) {
return String.format("<%s/api/users?page=%d&size=%d>; rel=\"next\"",
baseUrl, page, size);
}
```
## Performance Considerations
### Database Optimization
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Page<UserResponse> findAll(Specification<User> spec, Pageable pageable) {
// Use projection to only fetch needed fields
Page<User> page = userRepository.findAll(spec, pageable);
return page.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
}
```
### Cache Pagination Results
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
public Page<UserResponse> findAll(Specification<User> spec, Pageable pageable) {
String cacheKey = "users:" + spec.hashCode() + ":" + pageable.hashCode();
return cacheManager.getCache("users").get(cacheKey, () -> {
Page<User> page = userRepository.findAll(spec, pageable);
return page.map(this::toResponse);
});
}
}
```

View File

@@ -0,0 +1,376 @@
# Spring Boot REST API Standards - References
Complete reference for REST API development with Spring Boot.
## HTTP Methods and Status Codes Reference
### HTTP Methods
| Method | Idempotent | Safe | Purpose | Typical Status |
|--------|-----------|------|---------|----------------|
| GET | Yes | Yes | Retrieve resource | 200, 304, 404 |
| POST | No | No | Create resource | 201, 400, 409 |
| PUT | Yes | No | Replace resource | 200, 204, 404 |
| PATCH | No | No | Partial update | 200, 204, 400 |
| DELETE | Yes | No | Remove resource | 204, 404 |
| HEAD | Yes | Yes | Like GET, no body | 200, 304, 404 |
| OPTIONS | Yes | Yes | Describe communication options | 200 |
### HTTP Status Codes
**2xx Success:**
- `200 OK` - Successful GET/PUT/PATCH
- `201 Created` - Successful POST (include Location header)
- `202 Accepted` - Async processing accepted
- `204 No Content` - Successful DELETE or POST with no content
- `206 Partial Content` - Range request successful
**3xx Redirection:**
- `301 Moved Permanently` - Resource permanently moved
- `304 Not Modified` - Cache valid, use local copy
- `307 Temporary Redirect` - Temporary redirect
**4xx Client Errors:**
- `400 Bad Request` - Invalid format or parameters
- `401 Unauthorized` - Authentication required
- `403 Forbidden` - Authenticated but not authorized
- `404 Not Found` - Resource doesn't exist
- `409 Conflict` - Constraint violation or conflict
- `422 Unprocessable Entity` - Validation failed (semantic error)
**5xx Server Errors:**
- `500 Internal Server Error` - Unexpected server error
- `502 Bad Gateway` - External service unavailable
- `503 Service Unavailable` - Server temporarily down
## Spring Web Annotations Reference
### Request Mapping Annotations
```java
@RestController // Combines @Controller + @ResponseBody
@RequestMapping("/api") // Base URL path
@GetMapping // GET requests
@PostMapping // POST requests
@PutMapping // PUT requests
@PatchMapping // PATCH requests
@DeleteMapping // DELETE requests
```
### Parameter Binding Annotations
```java
@PathVariable // URL path variable /{id}
@RequestParam // Query string parameter ?page=0
@RequestParam(required=false) // Optional parameter
@RequestParam(defaultValue="10") // Default value
@RequestBody // Request body JSON/XML
@RequestHeader // HTTP header value
@CookieValue // Cookie value
@MatrixVariable // Matrix variable ;color=red
@Valid // Enable validation
```
### Response Annotations
```java
@ResponseBody // Serialize to response body
@ResponseStatus(status=HttpStatus.CREATED) // HTTP status
ResponseEntity<T> // Full response control
ResponseEntity.ok(body) // 200 OK
ResponseEntity.created(uri).body(body) // 201 Created
ResponseEntity.noContent().build() // 204 No Content
ResponseEntity.notFound().build() // 404 Not Found
```
## DTO Patterns Reference
### Request DTO (using Records)
```java
public record CreateProductRequest(
@NotBlank(message = "Name required") String name,
@NotNull @DecimalMin("0.01") BigDecimal price,
String description,
@NotNull @Min(0) Integer stock
) {}
```
### Response DTO (using Records)
```java
public record ProductResponse(
Long id,
String name,
BigDecimal price,
Integer stock,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
```
### Update DTO
```java
public record UpdateProductRequest(
@NotBlank String name,
@NotNull @DecimalMin("0.01") BigDecimal price,
String description
) {}
```
## Validation Annotations Reference
### Common Constraints
```java
@NotNull // Cannot be null
@NotEmpty // Collection/String cannot be empty
@NotBlank // String cannot be null/blank
@Size(min=1, max=255) // Length validation
@Min(value=1) // Minimum numeric value
@Max(value=100) // Maximum numeric value
@Positive // Must be positive
@Negative // Must be negative
@Email // Valid email format
@Pattern(regexp="...") // Regex validation
@Future // Date must be future
@Past // Date must be past
@Digits(integer=5, fraction=2) // Numeric precision
@DecimalMin("0.01") // Decimal minimum
@DecimalMax("9999.99") // Decimal maximum
```
### Custom Validation
```java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserRepository repository;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return !repository.existsByEmail(email);
}
}
```
## Pagination Reference
### Pageable Request Building
```java
// Basic pagination
Pageable pageable = PageRequest.of(page, size);
// With sorting
Sort sort = Sort.by("createdAt").descending();
Pageable pageable = PageRequest.of(page, size, sort);
// Multiple sort fields
Sort sort = Sort.by("status").ascending()
.and(Sort.by("createdAt").descending());
Pageable pageable = PageRequest.of(page, size, sort);
```
### Pagination Response Format
```json
{
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" }
],
"pageable": {
"offset": 0,
"pageNumber": 0,
"pageSize": 20,
"paged": true
},
"totalElements": 100,
"totalPages": 5,
"last": false,
"size": 20,
"number": 0,
"numberOfElements": 20,
"first": true,
"empty": false
}
```
## Error Response Format
### Standardized Error Response
```json
{
"status": 400,
"error": "Bad Request",
"message": "Validation failed: name: Name is required",
"path": "/api/v1/products",
"timestamp": "2024-01-15T10:30:00Z"
}
```
### Global Exception Handler Pattern
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
// Handle validation errors
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
// Handle not found
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
// Handle generic exceptions
}
}
```
## Content Negotiation
### Accept Header Examples
```
Accept: application/json # JSON response
Accept: application/xml # XML response
Accept: application/vnd.api+json # JSON:API standard
Accept: text/csv # CSV response
```
### Controller Implementation
```java
@GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/xml"})
public ResponseEntity<ProductResponse> getProduct(@PathVariable Long id) {
// Supports both JSON and XML
return ResponseEntity.ok(productService.findById(id));
}
```
## Pagination Best Practices
```java
// Limit maximum page size
@GetMapping
public ResponseEntity<Page<ProductResponse>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20")
@Max(value = 100, message = "Max page size is 100") int size) {
Pageable pageable = PageRequest.of(page, size);
return ResponseEntity.ok(productService.findAll(pageable));
}
```
## Maven Dependencies
```xml
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Data JPA (for Pageable) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
```
## Testing REST APIs
### MockMvc Testing
```java
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateProduct() throws Exception {
mockMvc.perform(post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Test\",\"price\":10.00}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
}
}
```
### TestRestTemplate Testing
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldFetchProduct() {
ResponseEntity<ProductResponse> response = restTemplate.getForEntity(
"/api/v1/products/1", ProductResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
```
## Related Skills
- **spring-boot-crud-patterns/SKILL.md** - CRUD operations following REST principles
- **spring-boot-dependency-injection/SKILL.md** - Dependency injection in controllers
- **spring-boot-test-patterns/SKILL.md** - Testing REST APIs
- **spring-boot-exception-handling/SKILL.md** - Global error handling
## External Resources
### Official Documentation
- [Spring Web MVC Documentation](https://docs.spring.io/spring-framework/reference/web/webmvc.html)
- [Spring REST Documentation](https://spring.io/guides/gs/rest-service/)
- [REST API Best Practices](https://restfulapi.net/)
### Related Standards
- [JSON:API Specification](https://jsonapi.org/)
- [OpenAPI Specification](https://www.openapis.org/)
- [RFC 7231 - HTTP Semantics](https://tools.ietf.org/html/rfc7231)
### Books
- "RESTful Web Services" by Leonard Richardson & Sam Ruby
- "Spring in Action" (latest edition)

View File

@@ -0,0 +1,521 @@
# Security Headers and CORS Configuration
## Security Headers Configuration
### Basic Security Headers
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self';")
.reportOnly(false))
.frameOptions(frame -> frame
.sameOrigin()
.deny()) // Use sameOrigin() for same-origin iframes, deny() to completely block
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000) // 1 year
.includeSubDomains(true)
.preload(true))
.xssProtection(xss -> xss
.headerValue(XssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentTypeOptions(contentTypeOptions -> contentTypeOptions
.and())
)
.cors(cors -> cors
.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Content-Type-Options"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
```
### Enhanced Security Configuration
```java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class EnhancedSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/**")
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' https:; " +
"frame-src 'none'; " +
"object-src 'none';"))
.frameOptions(frameOptions -> frameOptions.sameOrigin())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true)
.includeSubDomains(true))
.permissionsPolicy(permissionsPolicy -> permissionsPolicy
.add("camera", "()")
.add("geolocation", "()")
.add("microphone", "()")
.add("payment", "()"))
.referrerPolicy(referrerPolicy -> referrerPolicy.noReferrer())
.and())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/auth/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Allowed origins (consider restricting to specific domains in production)
configuration.setAllowedOriginPatterns(List.of("https://yourdomain.com", "https://app.yourdomain.com"));
// Allowed methods
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
// Allowed headers
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"Accept",
"X-Requested-With",
"X-Content-Type-Options",
"X-Total-Count",
"Cache-Control"
));
// Exposed headers to client
configuration.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-Content-Type-Options",
"Cache-Control"
));
// Allow credentials
configuration.setAllowCredentials(true);
// Cache preflight requests for 1 hour
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
```
## Content Security Policy (CSP)
### Basic CSP Configuration
```java
@Configuration
public class ContentSecurityPolicyConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ContentSecurityPolicyInterceptor());
}
};
}
}
@Component
public class ContentSecurityPolicyInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
}
}
```
### Advanced CSP with Nonce
```java
@Component
@RequiredArgsConstructor
public class SecurityHeadersFilter extends OncePerRequestFilter {
private final AtomicLong nonceCounter = new AtomicLong(0);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Generate nonce for each request
String nonce = String.valueOf(nonceCounter.incrementAndGet());
// Set CSP header with nonce for inline scripts
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'nonce-" + nonce + "'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
// Add nonce to request attributes for templates
request.setAttribute("cspNonce", nonce);
// Set other security headers
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
response.setHeader("X-Permitted-Cross-Domain-Policies", "none");
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
filterChain.doFilter(request, response);
}
}
```
## CORS Configuration
### Method-level CORS
```java
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "https://yourdomain.com", methods = {RequestMethod.GET, RequestMethod.POST})
public class UserController {
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
// CORS allowed for GET requests
return ResponseEntity.ok(userService.findAll());
}
@PostMapping
@CrossOrigin(origins = "https://app.yourdomain.com")
public ResponseEntity<User> createUser(@RequestBody User user) {
// CORS allowed with different origin for POST requests
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(user));
}
}
```
### Dynamic CORS Configuration
```java
@Configuration
public class DynamicCorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// Development configuration
CorsConfiguration devConfig = new CorsConfiguration();
devConfig.setAllowedOriginPatterns(List.of("*"));
devConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
devConfig.setAllowedHeaders(Arrays.asList("*"));
devConfig.setAllowCredentials(true);
source.registerCorsConfiguration("/api/**", devConfig);
// Production configuration - restrict to specific domains
CorsConfiguration prodConfig = new CorsConfiguration();
prodConfig.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://app.yourdomain.com",
"https://api.yourdomain.com"
));
prodConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
prodConfig.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"Accept",
"X-Requested-With"
));
prodConfig.setExposedHeaders(Arrays.asList("X-Total-Count"));
prodConfig.setAllowCredentials(true);
source.registerCorsConfiguration("/api/**", prodConfig);
return source;
}
}
```
## Security Headers Best Practices
### Essential Headers for Production
1. **Content-Security-Policy**: Mitigates XSS attacks
2. **X-Content-Type-Options**: Prevents MIME type sniffing
3. **X-Frame-Options**: Prevents clickjacking
4. **Strict-Transport-Security**: Enforces HTTPS
5. **X-XSS-Protection**: Legacy browser XSS protection
6. **Referrer-Policy**: Controls referrer information
### CSP Examples by Application Type
#### Blog/Content Site
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' https: data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src https://www.youtube.com; " +
"media-src https://www.youtube.com;");
```
#### Single Page Application (SPA)
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self' wss:; " +
"frame-src 'none'; " +
"object-src 'none';");
```
#### API Only
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
```
### Security Header Testing
```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class SecurityHeadersTest {
@Autowired
private MockMvc mockMvc;
@Test
void securityHeaders_shouldBeSet() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(header().string("Content-Security-Policy", notNullValue()))
.andExpect(header().string("X-Content-Type-Options", "nosniff"))
.andExpect(header().string("X-Frame-Options", notNullValue()))
.andExpect(header().string("Strict-Transport-Security", notNullValue()));
}
}
```
## Rate Limiting
### Basic Rate Limiting
```java
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final ConcurrentHashMap<String, RateLimit> rateLimits = new ConcurrentHashMap<>();
private static final long REQUEST_LIMIT = 100;
private static final long TIME_WINDOW = 60_000; // 1 minute
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
String path = request.getRequestURI();
String key = clientIp + ":" + path;
RateLimit rateLimit = rateLimits.computeIfAbsent(key, k -> new RateLimit());
synchronized (rateLimit) {
if (System.currentTimeMillis() - rateLimit.resetTime > TIME_WINDOW) {
rateLimit.count = 0;
rateLimit.resetTime = System.currentTimeMillis();
}
if (rateLimit.count >= REQUEST_LIMIT) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded");
return;
}
rateLimit.count++;
}
filterChain.doFilter(request, response);
}
private static class RateLimit {
long count = 0;
long resetTime = System.currentTimeMillis();
}
}
```
## Token-based Authentication Headers
```java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authentication =
jwtTokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
```
## WebSocket Security
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("https://yourdomain.com")
.withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Validate token and authenticate
String token = accessor.getFirstNativeHeader("Authorization");
if (!isValidToken(token)) {
throw new UnauthorizedWebSocketException("Invalid token");
}
}
return message;
}
});
}
}
```

View File

@@ -0,0 +1,309 @@
# Spring Web Annotations Reference
## Controller and Mapping Annotations
### @RestController
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
// Returns JSON responses automatically
}
```
### @Controller
```java
@Controller
@RequestMapping("/users")
public class UserController {
// Returns view names for MVC applications
}
```
### @RequestMapping
```java
// Class level
@RequestMapping("/api")
@RequestMapping(path = "/api", method = RequestMethod.GET)
// Method level
@RequestMapping("/users")
@RequestMapping(path = "/users", method = RequestMethod.POST)
```
### HTTP Method Annotations
```java
@GetMapping("/users")
public List<User> getUsers() { ... }
@PostMapping("/users")
public User createUser(@RequestBody User user) { ... }
@PutMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
@PatchMapping("/users/{id}")
public User patchUser(@PathVariable Long id, @RequestBody User user) { ... }
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) { ... }
@HeadMapping("/users/{id}")
public ResponseEntity<Void> headUser(@PathVariable Long id) { ... }
@OptionsMapping("/users")
public ResponseEntity<Void> optionsUsers() { ... }
```
## Parameter Binding Annotations
### @PathVariable
```java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// Multiple path variables
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(@PathVariable Long userId, @PathVariable Long orderId) { ... }
// Custom variable name
@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") Long id) { ... }
```
### @RequestParam
```java
@GetMapping("/users")
public List<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
// Handle pagination, filtering, and sorting
}
```
### @RequestBody
```java
@PostMapping("/users")
public User createUser(@RequestBody User user) { ... }
// With validation
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) { ... }
```
### @RequestHeader
```java
@GetMapping("/users")
public List<User> getUsers(@RequestHeader("Authorization") String authHeader) { ... }
// Multiple headers
@PostMapping("/users")
public User createUser(
@RequestBody User user,
@RequestHeader("X-Custom-Header") String customHeader) { ... }
```
### @CookieValue
```java
@GetMapping("/users")
public List<User> getUsers(@CookieValue("JSESSIONID") String sessionId) { ... }
```
### @MatrixVariable
```java
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id,
@MatrixVariable(pathVar = "id", required = false) Map<String, String> params) {
// Handle matrix variables: /users/123;name=John;age=30
}
```
## Response Annotations
### @ResponseStatus
```java
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) { ... }
```
### @ResponseBody
```java
@Controller
public class UserController {
@GetMapping("/users")
@ResponseBody
public List<User> getUsers() { ... }
}
```
### ResponseEntity
```java
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.create(user);
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.body(created);
}
```
## Content Negotiation
### Produces and Consumes
```java
@GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public List<User> getUsers() { ... }
@PostMapping(value = "/users", consumes = MediaType.APPLICATION_JSON_VALUE)
public User createUser(@RequestBody User user) { ... }
// Multiple media types
@GetMapping(value = "/users", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public List<User> getUsers() { ... }
```
### @RequestBody with Content-Type
```java
@PostMapping(value = "/users", consumes = "application/json")
public User createUserJson(@RequestBody User user) { ... }
@PostMapping(value = "/users", consumes = "application/xml")
public User createUserXml(@RequestBody User user) { ... }
```
## Validation Annotations
### @Valid
```java
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) { ... }
// Validates individual parameters
@GetMapping("/users")
public User getUser(
@Valid @Pattern(regexp = "^[a-zA-Z0-9]+$") @PathVariable String id) { ... }
```
### Jakarta Bean Validation Annotations
```java
public class UserRequest {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Valid email required")
private String email;
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
private String password;
@Min(value = 18, message = "Must be at least 18")
@Max(value = 120, message = "Invalid age")
private Integer age;
@Pattern(regexp = "^[A-Z][a-z]+$", message = "Invalid name format")
private String firstName;
@NotEmpty(message = "At least one role required")
private Set<String> roles = new HashSet<>();
@Future(message = "Date must be in the future")
private LocalDate futureDate;
@Past(message = "Date must be in the past")
private LocalDate birthDate;
@Positive(message = "Value must be positive")
private Double positiveValue;
@PositiveOrZero(message = "Value must be positive or zero")
private Double nonNegativeValue;
}
```
## Specialized Annotations
### @RestControllerAdvice
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
// Handle validation errors globally
}
}
```
### @ExceptionHandler
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NoHandlerFoundException ex) {
return ResponseEntity.notFound().build();
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
```
### @CrossOrigin
```java
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api/users")
public class UserController {
// Enable CORS for specific origin
}
// Or at method level
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST})
@GetMapping("/users")
public List<User> getUsers() { ... }
```
## Async Processing
### @Async
```java
@Service
public class AsyncService {
@Async
public CompletableFuture<User> processUser(User user) {
// Long-running operation
return CompletableFuture.completedFuture(processedUser);
}
}
@RestController
public class UserController {
@GetMapping("/users/{id}/async")
public CompletableFuture<ResponseEntity<User>> getUserAsync(@PathVariable Long id) {
return userService.processUser(id)
.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}
}
```