Getting started
Introduction
Spring Boot makes it easy to create stand-alone, production-grade Spring applications. It provides auto-configuration, embedded servers, and starter dependencies.
- Current Version: 4.0.2
- Java Versions: 17, 21, 23
- Build Tools: Maven, Gradle (Groovy/Kotlin)
Spring Initializr
# Web-based generator
https://start.spring.io
# Command line (curl)
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa \
-d type=maven-project \
-d language=java \
-d bootVersion=4.0.2 \
-d baseDir=my-app \
-o my-app.zip
# Spring CLI
spring init --dependencies=web,data-jpa my-app
Generate projects with dependencies, metadata, and build configuration.
Maven Dependencies
<!-- Parent POM -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.2</version>
</parent>
<dependencies>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA/Hibernate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Gradle Dependencies
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.2'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Main Application Class
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication combines @Configuration, @EnableAutoConfiguration, and @ComponentScan.
Core Annotations
Application Annotations
| Annotation | Purpose |
|---|---|
@SpringBootApplication |
Main application class |
@EnableAutoConfiguration |
Enable auto-configuration |
@ComponentScan |
Scan for components |
@Configuration |
Java-based configuration |
@Bean |
Define a bean |
Stereotype Annotations
| Annotation | Purpose |
|---|---|
@Component |
Generic component |
@Service |
Service layer |
@Repository |
Data access layer |
@Controller |
MVC controller |
@RestController |
REST API controller |
Dependency Injection
// Constructor injection (recommended)
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
// Field injection
@Service
public class UserService {
@Autowired
private UserRepository repository;
}
// Setter injection
@Service
public class UserService {
private UserRepository repository;
@Autowired
public void setRepository(UserRepository repository) {
this.repository = repository;
}
}
Configuration Annotations
| Annotation | Purpose |
|---|---|
@Value("${property}") |
Inject property value |
@ConfigurationProperties |
Bind properties to class |
@Profile |
Activate for profile |
@Conditional |
Conditional bean creation |
@PropertySource |
Load property file |
REST Controllers
Basic REST Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
HTTP Mapping Annotations
| Annotation | HTTP Method |
|---|---|
@GetMapping |
GET |
@PostMapping |
POST |
@PutMapping |
PUT |
@DeleteMapping |
DELETE |
@PatchMapping |
PATCH |
@RequestMapping |
Any/Custom |
Request/Response Handling
@RestController
@RequestMapping("/api")
public class ApiController {
// Path variable
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { }
// Request parameter
@GetMapping("/users")
public List<User> searchUsers(@RequestParam String name) { }
// Request body
@PostMapping("/users")
public User createUser(@RequestBody User user) { }
// Response status
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) { }
// Response entity
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(user);
}
}
Exception Handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception ex) {
return new ErrorResponse("Internal server error");
}
}
Configuration
application.properties
# Server
server.port=8080
server.servlet.context-path=/api
# Database
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
# JPA/Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# Logging
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.file.name=logs/app.log
# Custom properties
app.name=My Application
app.version=1.0.0
application.yml
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
logging:
level:
root: INFO
com.example: DEBUG
file:
name: logs/app.log
app:
name: My Application
version: 1.0.0
Profile-Specific Configuration
# application.yml (default)
spring:
profiles:
active: dev
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-server:3306/proddb
# Run with specific profile
java -jar app.jar --spring.profiles.active=prod
Configuration Properties Class
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private String name;
private String version;
private Security security = new Security();
// Getters and setters
public static class Security {
private String secret;
private int tokenExpiration;
// Getters and setters
}
}
app:
name: My Application
version: 1.0.0
security:
secret: my-secret-key
token-expiration: 3600
Common Starters
Web & REST
| Starter | Purpose |
|---|---|
spring-boot-starter-web |
Web applications, REST APIs |
spring-boot-starter-webflux |
Reactive web applications |
spring-boot-starter-websocket |
WebSocket support |
spring-boot-starter-validation |
Bean validation (JSR-380) |
spring-boot-starter-hateoas |
Hypermedia REST APIs |
Data Access
| Starter | Purpose |
|---|---|
spring-boot-starter-data-jpa |
JPA with Hibernate |
spring-boot-starter-data-mongodb |
MongoDB support |
spring-boot-starter-data-redis |
Redis support |
spring-boot-starter-jdbc |
JDBC with connection pooling |
spring-boot-starter-data-rest |
REST repositories |
Security & Messaging
| Starter | Purpose |
|---|---|
spring-boot-starter-security |
Spring Security |
spring-boot-starter-oauth2-client |
OAuth2/OIDC client |
spring-boot-starter-amqp |
RabbitMQ messaging |
spring-boot-starter-artemis |
JMS with Apache Artemis |
Testing & DevTools
| Starter | Purpose |
|---|---|
spring-boot-starter-test |
Testing (JUnit, Mockito, AssertJ) |
spring-boot-devtools |
Hot reload, LiveReload |
spring-boot-starter-actuator |
Production monitoring |
Data Access (JPA)
Entity Class
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
// Constructors, getters, setters
}
Repository Interface
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query methods (derived from method name)
List<User> findByName(String name);
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String keyword);
// Custom query with JPQL
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE created_at > ?1",
nativeQuery = true)
List<User> findRecentUsers(LocalDateTime date);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateUserName(@Param("id") Long id, @Param("name") String name);
}
Service Layer
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}
@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@Transactional
public User save(User user) {
return userRepository.save(user);
}
@Transactional
public void delete(Long id) {
userRepository.deleteById(id);
}
}
Testing
Unit Test
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldFindUserById() {
User user = new User(1L, "test@example.com", "Test User");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
User result = userService.findById(1L);
assertThat(result).isNotNull();
assertThat(result.getEmail()).isEqualTo("test@example.com");
verify(userRepository).findById(1L);
}
}
Integration Test
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldCreateUser() throws Exception {
User user = new User(null, "test@example.com", "Test User");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
}
Repository Test
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldFindByEmail() {
User user = new User(null, "test@example.com", "Test User");
userRepository.save(user);
Optional<User> result = userRepository.findByEmail("test@example.com");
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("Test User");
}
}
Test Properties
# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
Actuator & Monitoring
Actuator Setup
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
Actuator Endpoints
| Endpoint | Purpose |
|---|---|
/actuator/health |
Application health |
/actuator/info |
Application info |
/actuator/metrics |
Application metrics |
/actuator/env |
Environment properties |
/actuator/loggers |
Logger configuration |
/actuator/beans |
Spring beans |
Custom Health Indicator
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Check custom condition
if (checkExternalService()) {
return Health.up()
.withDetail("service", "available")
.build();
}
return Health.down()
.withDetail("service", "unavailable")
.build();
}
private boolean checkExternalService() {
// Implementation
return true;
}
}
Common Patterns
DTO Pattern
// Entity
@Entity
public class User {
@Id
private Long id;
private String email;
private String password; // Sensitive
}
// DTO
public class UserDTO {
private Long id;
private String email;
// No password field
public static UserDTO fromEntity(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setEmail(user.getEmail());
return dto;
}
}
// Controller
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.findById(id);
return UserDTO.fromEntity(user);
}
Builder Pattern
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private Long id;
private String email;
private String name;
}
// Usage
User user = User.builder()
.email("test@example.com")
.name("Test User")
.build();
Event Publishing
// Event
public class UserCreatedEvent extends ApplicationEvent {
private final User user;
public UserCreatedEvent(Object source, User user) {
super(source);
this.user = user;
}
}
// Publisher
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public User createUser(User user) {
User saved = userRepository.save(user);
eventPublisher.publishEvent(new UserCreatedEvent(this, saved));
return saved;
}
}
// Listener
@Component
public class UserEventListener {
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
// Send welcome email, etc.
}
}
Running & Packaging
Maven Commands
# Run application
./mvnw spring-boot:run
# Run with profile
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Package JAR
./mvnw clean package
# Skip tests
./mvnw clean package -DskipTests
# Run JAR
java -jar target/app-1.0.0.jar
# Run JAR with profile
java -jar target/app-1.0.0.jar --spring.profiles.active=prod
Gradle Commands
# Run application
./gradlew bootRun
# Run with profile
./gradlew bootRun --args='--spring.profiles.active=dev'
# Package JAR
./gradlew clean build
# Skip tests
./gradlew clean build -x test
# Run JAR
java -jar build/libs/app-1.0.0.jar
Executable JAR
<!-- Maven -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
# Make JAR executable
chmod +x target/app-1.0.0.jar
# Run directly
./target/app-1.0.0.jar
Docker
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# Build image
docker build -t my-app:1.0.0 .
# Run container
docker run -p 8080:8080 my-app:1.0.0
Best Practices
Constructor Injection
Always prefer constructor injection over field injection:
// Good
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
// Avoid
@Service
public class UserService {
@Autowired
private UserRepository repository;
}
Transaction Management
Use @Transactional at service layer:
@Service
public class UserService {
@Transactional(readOnly = true)
public List<User> findAll() {
return repository.findAll();
}
@Transactional
public User save(User user) {
return repository.save(user);
}
}
Exception Handling
Create custom exceptions and global handlers:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found with id: " + id);
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
}
Configuration Validation
Validate configuration properties:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
@Min(1024)
@Max(65535)
private int port;
// Getters and setters
}
Also see
- Spring Initializr - Project generator
- Spring Boot Documentation - Official docs
- Spring Boot Guides - Step-by-step tutorials
- Spring Boot Starters - Available starters list
- Spring Data JPA - JPA reference
- Spring Security - Security framework