Spring Boot
Spring Boot Annotations Overview
Spring Boot is annotation-driven — nearly every behavior is configured through annotations rather than XML or boilerplate code. Understanding what each annotation does, where it belongs, and how annotations interact gives you full control over your Spring Boot application.
Core Spring Boot Annotations
These are the foundational annotations that every Spring Boot application uses. They control application startup, component detection, and the IoC container.
Java
// @SpringBootApplication — the entry point annotation:
// Combines @Configuration + @EnableAutoConfiguration + @ComponentScan
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// @SpringBootApplication with customization:
@SpringBootApplication(
scanBasePackages = {"com.example.myapp", "com.example.shared"},
exclude = {SecurityAutoConfiguration.class}
)
public class Application { }
// @EnableAutoConfiguration — enables Spring Boot's auto-configuration:
// Reads META-INF/spring/AutoConfiguration.imports and applies matching configs
// Rarely used directly — @SpringBootApplication includes it
@EnableAutoConfiguration
// @ComponentScan — tells Spring where to look for components:
// By default scans the package of the annotated class and all sub-packages
@ComponentScan(basePackages = "com.example.myapp")
// @Configuration — marks a class as a source of bean definitions:
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// @Bean — declares a method's return value as a Spring-managed bean:
@Bean
@Primary // preferred when multiple beans of same type exist
@Scope("prototype") // new instance on every request (default is singleton)
@Lazy // don't create until first requested
@DependsOn("otherBean") // ensure otherBean is created first
public MyService myService() {
return new MyServiceImpl();
}
// @Import — explicitly import another @Configuration class:
@Configuration
@Import({SecurityConfig.class, CacheConfig.class})
public class AppConfig { }Stereotype Annotations
Stereotype annotations mark classes as Spring-managed components. Each stereotype communicates the role of the class and may carry additional behavior.
Java
// @Component — generic Spring-managed component:
// Used when no more specific stereotype fits
@Component
public class EmailTemplateRenderer {
public String render(String template, Map<String, Object> model) { ... }
}
// @Service — business logic layer:
// Semantically identical to @Component — communicates intent
// May be enhanced with transaction management in future Spring versions
@Service
@Transactional
public class UserService {
public User createUser(CreateUserRequest request) { ... }
}
// @Repository — data access layer:
// Extends @Component with exception translation:
// SQL exceptions → Spring DataAccessException hierarchy
// Enables @Transactional rollback on DataAccessException
@Repository
public class UserRepositoryImpl implements UserRepository {
public User save(User user) { ... }
}
// @Controller — web MVC controller (returns views):
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome!");
return "home"; // resolves to templates/home.html
}
}
// @RestController — REST API controller (returns JSON/XML):
// Combines @Controller + @ResponseBody on every method
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
// All stereotype annotations support:
@Component("customBeanName") // specify a custom bean name
@Scope("prototype") // change the scope
@Lazy // lazy initialization
@Primary // preferred candidate when multiple beans exist
@Qualifier("specificImpl") // disambiguate when multiple implementations existDependency Injection Annotations
These annotations control how Spring injects dependencies into your beans. Constructor injection is preferred — it makes dependencies explicit, allows final fields, and keeps classes testable without Spring.
Java
// @Autowired — inject a dependency automatically by type:
@Service
public class OrderService {
// Constructor injection (RECOMMENDED — field can be final):
private final OrderRepository repository;
private final PaymentService paymentService;
@Autowired // optional when there's only one constructor (Spring 4.3+)
public OrderService(OrderRepository repository, PaymentService paymentService) {
this.repository = repository;
this.paymentService = paymentService;
}
}
// Setter injection — for optional dependencies:
@Autowired(required = false)
public void setMetricsCollector(MetricsCollector collector) {
this.collector = collector;
}
// Field injection — convenient but avoid in production code:
@Autowired
private UserRepository userRepository; // can't be final, hard to test
// @Qualifier — disambiguate when multiple beans of the same type exist:
@Autowired
@Qualifier("primaryDataSource")
private DataSource dataSource;
// @Primary — mark a bean as the default when multiple exist:
@Bean
@Primary
public DataSource primaryDataSource() { ... }
// @Bean
public DataSource secondaryDataSource() { ... }
// @Value — inject property values:
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.max-retries:3}") // with default value
private int maxRetries;
@Value("#{systemProperties['java.home']}") // SpEL expression
private String javaHome;
@Value("#{T(java.lang.Math).PI}") // SpEL with static method
private double pi;
// @ConfigurationProperties — bind a group of properties to a class:
@ConfigurationProperties(prefix = "app.mail")
@Component
public class MailProperties {
private String host;
private int port = 587;
private boolean ssl = true;
// getters and setters required
}Web MVC Annotations
Web annotations handle HTTP request mapping, parameter extraction, response formatting, and cross-cutting web concerns. These are the annotations you use in every REST controller.
Java
// Request mapping annotations:
@RequestMapping("/api/users") // maps any HTTP method
@GetMapping("/api/users") // GET only
@PostMapping("/api/users") // POST only
@PutMapping("/api/users/{id}") // PUT only
@PatchMapping("/api/users/{id}") // PATCH only
@DeleteMapping("/api/users/{id}") // DELETE only
// Request mapping with options:
@GetMapping(
value = "/users",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
// Parameter extraction:
@GetMapping("/{id}")
public UserResponse getUser(
@PathVariable Long id, // from URL path
@RequestParam(defaultValue = "false") boolean active, // query param
@RequestParam(required = false) String search, // optional query param
@RequestHeader("Authorization") String token, // from HTTP header
@CookieValue("sessionId") String session, // from cookie
@RequestBody @Valid CreateUserRequest request, // from request body (JSON)
@RequestAttribute("currentUser") User user // from request attributes
) { }
// Response control:
@ResponseStatus(HttpStatus.CREATED) // set HTTP status code
@ResponseBody // serialize return value to response body
// (included in @RestController)
// Cross-origin resource sharing:
@CrossOrigin(origins = "https://myapp.com") // on a single controller or method
@CrossOrigin(origins = "*", maxAge = 3600) // allow all origins
// Exception handling:
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
// Model attributes (MVC — not REST):
@ModelAttribute("categories")
public List<String> categories() {
return categoryService.findAll(); // added to every model in this controller
}
// Controller advice — applies to all controllers:
@RestControllerAdvice // @ControllerAdvice + @ResponseBody
@ControllerAdvice // for non-REST controllersData and JPA Annotations
JPA and Spring Data annotations map Java classes to database tables and define query behavior. These annotations live in the model package on entity classes and in the repository interfaces.
Java
// Entity definition:
@Entity // marks class as a JPA entity
@Table(name = "users") // maps to specific table name
@Table(name = "users", schema = "myapp") // with schema
@EntityListeners(AuditingEntityListener.class) // enable @CreatedDate etc.
public class User {
// Primary key:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment
private Long id;
// Column mapping:
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(name = "full_name", nullable = false)
private String name;
// Auditing:
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
// Relationships:
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
@ManyToMany
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", referencedColumnName = "id")
private UserProfile profile;
// Embedded object:
@Embedded
private Address address;
// Enumeration:
@Enumerated(EnumType.STRING) // store enum name as String (not ordinal)
private UserStatus status;
// Transient — not persisted:
@Transient
private String temporaryToken;
}
// Enable JPA auditing in a config class:
@Configuration
@EnableJpaAuditing
public class JpaConfig { }
// Repository annotations:
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") Long id);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<User> findById(Long id);Transaction and Cache Annotations
Transaction and cache annotations apply cross-cutting behavior to service methods without cluttering business logic code. They work through Spring AOP — the annotations are intercepted at runtime.
Java
// @Transactional — wrap a method in a database transaction:
@Service
public class OrderService {
// Default — rolls back on RuntimeException, commits on checked exception:
@Transactional
public Order createOrder(OrderRequest request) { ... }
// Read-only optimization — skips dirty checking, uses read-only connection:
@Transactional(readOnly = true)
public List<Order> findAll() { ... }
// Custom rollback — rollback on checked exception too:
@Transactional(rollbackFor = Exception.class)
public void processPayment(Payment payment) throws PaymentException { ... }
// No rollback for a specific exception:
@Transactional(noRollbackFor = OptimisticLockException.class)
public Order updateOrder(Long id, OrderUpdate update) { ... }
// Propagation — controls transaction boundaries:
@Transactional(propagation = Propagation.REQUIRED) // default: join or create
@Transactional(propagation = Propagation.REQUIRES_NEW) // always create new
@Transactional(propagation = Propagation.SUPPORTS) // join if exists, else none
@Transactional(propagation = Propagation.NOT_SUPPORTED) // suspend if exists
@Transactional(propagation = Propagation.NEVER) // fail if transaction exists
// Isolation level:
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.SERIALIZABLE)
// Timeout:
@Transactional(timeout = 30) // rollback if method takes > 30 seconds
}
// @Cacheable — cache the return value:
@Service
@EnableCaching // (on a @Configuration class, not here)
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
// On first call: executes method, caches result
// On subsequent calls: returns cached result, skips method
}
@Cacheable(value = "products", condition = "#id > 0", unless = "#result == null")
public Product findByIdConditional(Long id) { ... }
// @CacheEvict — remove from cache:
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) { ... }
@CacheEvict(value = "products", allEntries = true) // clear entire cache
public void clearProductCache() { }
// @CachePut — update cache without skipping method:
@CachePut(value = "products", key = "#result.id")
public Product updateProduct(UpdateProductRequest request) { ... }
// @Caching — multiple cache operations on one method:
@Caching(
evict = { @CacheEvict("products"), @CacheEvict("categories") }
)
public void clearAll() { }
}Validation Annotations
Validation annotations constrain the values of fields, method parameters, and return values. They're evaluated by Hibernate Validator when @Valid or @Validated is used.
Java
// On request DTO fields:
public record CreateUserRequest(
@NotNull(message = "Email cannot be null")
@NotBlank(message = "Email cannot be blank")
@Email(message = "Must be a valid email address")
@Size(max = 255, message = "Email cannot exceed 255 characters")
String email,
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
@Pattern(regexp = "^[a-zA-Z ]+$", message = "Name can only contain letters and spaces")
String name,
@NotNull
@Min(value = 0, message = "Age cannot be negative")
@Max(value = 150, message = "Age must be realistic")
Integer age,
@NotNull
@DecimalMin(value = "0.0", inclusive = false, message = "Price must be positive")
@Digits(integer = 8, fraction = 2, message = "Price format invalid")
BigDecimal price,
@NotEmpty(message = "Tags list cannot be empty")
@Size(max = 10, message = "Maximum 10 tags allowed")
List<@NotBlank String> tags,
@Future(message = "Delivery date must be in the future")
LocalDate deliveryDate,
@Past(message = "Birth date must be in the past")
LocalDate birthDate
) { }
// Trigger validation in controllers:
@PostMapping("/users")
public UserResponse createUser(@RequestBody @Valid CreateUserRequest request) { }
// Validate method parameters (add @Validated to class):
@Service
@Validated
public class UserService {
public User findByEmail(@NotBlank @Email String email) { ... }
public List<User> findPage(@Min(0) int page, @Max(100) int size) { ... }
}
// Validate return values:
@Validated
public class UserService {
@NotNull
public User createUser(CreateUserRequest request) { ... }
}
// Custom validator:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already in use";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired private UserRepository userRepository;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return email != null && !userRepository.existsByEmail(email);
}
}Security Annotations
Spring Security provides method-level security annotations that restrict access based on roles and permissions. These annotations work through AOP — the security check runs before the method executes.
Java
// Enable method security on a @Configuration class:
@Configuration
@EnableMethodSecurity // Spring Security 6+ (replaces @EnableGlobalMethodSecurity)
public class SecurityConfig { }
// @PreAuthorize — check BEFORE method executes (most commonly used):
@Service
public class UserService {
// Only users with ADMIN role can call this:
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAllUsers() { ... }
// Multiple roles — either role is sufficient:
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public void deactivateUser(Long id) { ... }
// Check a permission (not just role):
@PreAuthorize("hasAuthority('user:delete')")
public void deleteUser(Long id) { ... }
// SpEL — access method parameters:
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public User getUserProfile(Long userId) { ... }
// Check that the current user owns the resource:
@PreAuthorize("@userSecurity.isOwner(authentication, #id)")
public Order getOrder(Long id) { ... }
}
// @PostAuthorize — check AFTER method executes (use when return value matters):
@PostAuthorize("returnObject.userId == authentication.principal.id")
public Order findOrder(Long id) { ... }
// @Secured — simpler role-based check (less flexible than @PreAuthorize):
@Secured("ROLE_ADMIN")
public void adminOnlyMethod() { ... }
// @RolesAllowed — JSR-250 standard annotation (same as @Secured):
@RolesAllowed({"ROLE_ADMIN", "ROLE_MANAGER"})
public void managerMethod() { ... }
// @PreFilter — filter input collection before method executes:
@PreFilter("filterObject.userId == authentication.principal.id")
public void processOrders(List<Order> orders) { ... }
// @PostFilter — filter return collection after method executes:
@PostFilter("filterObject.userId == authentication.principal.id")
public List<Order> getOrders() { ... }Scheduling and Async Annotations
Scheduling annotations run methods on a fixed schedule or in background threads. Both require enabling on a @Configuration class before they take effect.
Java
// Enable scheduling — add to any @Configuration class:
@Configuration
@EnableScheduling
public class SchedulingConfig { }
// Enable async — add to any @Configuration class:
@Configuration
@EnableAsync
public class AsyncConfig {
// Custom thread pool for async execution:
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
// @Scheduled — run on a schedule:
@Component
public class ScheduledTasks {
// Fixed delay — wait N ms AFTER the last execution completes:
@Scheduled(fixedDelay = 5000)
public void runAfterDelay() { }
// Fixed rate — run every N ms regardless of last execution duration:
@Scheduled(fixedRate = 10000)
public void runEvery10Seconds() { }
// Initial delay — wait before first execution:
@Scheduled(fixedRate = 60000, initialDelay = 30000)
public void runAfterStartup() { }
// Cron expression — for precise scheduling:
@Scheduled(cron = "0 0 2 * * ?") // every day at 2:00 AM
public void dailyCleanup() { }
@Scheduled(cron = "0 0 9 ? * MON-FRI") // weekdays at 9:00 AM
public void weekdayReport() { }
@Scheduled(cron = "0 */15 * * * ?") // every 15 minutes
public void frequentTask() { }
// Cron with timezone:
@Scheduled(cron = "0 0 9 * * ?", zone = "Asia/Kolkata")
public void indiaTimezoneTask() { }
}
// @Async — run a method in a background thread:
@Service
public class EmailService {
@Async
public void sendWelcomeEmail(String to, String name) {
// Runs in a background thread — caller returns immediately
mailSender.send(buildWelcomeEmail(to, name));
}
// @Async with return value — returns a Future:
@Async
public CompletableFuture<String> generateReport(Long userId) {
String report = buildReport(userId); // runs in background
return CompletableFuture.completedFuture(report);
}
}
// Call async method:
emailService.sendWelcomeEmail("alice@test.com", "Alice"); // returns immediately
CompletableFuture<String> future = reportService.generateReport(1L);
String report = future.get(30, TimeUnit.SECONDS); // wait up to 30sProfile and Conditional Annotations
Profile and conditional annotations control which beans are created based on the runtime environment — active profiles, classpath contents, properties, and bean existence.
Java
// @Profile — create bean only in specific profiles:
@Component
@Profile("dev")
public class MockEmailService implements EmailService {
public void send(String to, String subject, String body) {
System.out.println("Mock email to: " + to);
}
}
@Component
@Profile("prod")
public class SmtpEmailService implements EmailService {
public void send(String to, String subject, String body) {
mailSender.send(to, subject, body);
}
}
// @Profile with NOT — active in every profile except prod:
@Component
@Profile("!prod")
public class DevDataSeeder implements CommandLineRunner {
public void run(String... args) {
seedTestData();
}
}
// Multiple profiles — active if ANY of these profiles are active:
@Component
@Profile({"dev", "test"})
public class LocalStorageService implements StorageService { }
// @Conditional — create bean based on a custom condition:
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}
@Bean
@Conditional(OnLinuxCondition.class)
public FileWatcher linuxFileWatcher() { ... }
// Built-in @Conditional annotations (same ones used in auto-configuration):
@ConditionalOnClass(DataSource.class) // if class is on classpath
@ConditionalOnMissingClass("com.example.Foo") // if class is NOT on classpath
@ConditionalOnBean(DataSource.class) // if bean exists
@ConditionalOnMissingBean(DataSource.class) // if bean does NOT exist
@ConditionalOnProperty(name = "feature.x", havingValue = "true")
@ConditionalOnWebApplication // if this is a web app
@ConditionalOnExpression("${feature.enabled:false}")
// Combine conditions:
@Configuration
@Profile("prod")
@ConditionalOnProperty(name = "app.payments.enabled", havingValue = "true")
public class ProductionPaymentConfig {
@Bean
public PaymentGateway stripeGateway() { ... }
}Testing Annotations
Spring Boot testing annotations load specific slices of the application context — letting you test each layer in isolation with only the beans that layer needs, keeping tests fast and focused.
Java
// @SpringBootTest — loads full application context:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class FullIntegrationTest {
@Autowired TestRestTemplate restTemplate;
}
// @WebMvcTest — loads only MVC layer (controllers, filters, advice):
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService; // mock service — not loaded by @WebMvcTest
@Autowired ObjectMapper objectMapper;
}
// @DataJpaTest — loads only JPA layer with in-memory H2:
@DataJpaTest
class UserRepositoryTest {
@Autowired UserRepository userRepository;
@Autowired TestEntityManager entityManager; // helper for test data setup
}
// @DataMongoTest — loads only MongoDB layer:
@DataMongoTest
class ProductRepositoryTest {
@Autowired ProductRepository productRepository;
}
// @RestClientTest — loads REST client beans (RestTemplate, WebClient):
@RestClientTest(WeatherClient.class)
class WeatherClientTest {
@Autowired WeatherClient weatherClient;
@Autowired MockRestServiceServer server; // mock the external HTTP server
}
// @MockBean — create a Mockito mock and register it as a Spring bean:
@MockBean
private UserService userService;
// when(userService.findById(1L)).thenReturn(user);
// @SpyBean — wrap a real bean with a Mockito spy:
@SpyBean
private EmailService emailService;
// verify(emailService, times(1)).sendWelcome(any(), any());
// @TestConfiguration — additional beans only for tests:
@TestConfiguration
static class TestConfig {
@Bean
public Clock fixedClock() {
return Clock.fixed(Instant.parse("2024-01-01T00:00:00Z"), ZoneOffset.UTC);
}
}
// @TestPropertySource — override properties for a test:
@SpringBootTest
@TestPropertySource(properties = {
"app.jwt.expiration=3600",
"spring.jpa.show-sql=true"
})
class ServiceTest { }
// @Sql — run SQL scripts before/after test:
@DataJpaTest
@Sql("/test-data/users.sql") // runs before each test
@Sql(scripts = "/test-data/cleanup.sql",
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class UserQueryTest { }