NexusCS

Spring Boot

Java
Spring Boot is an opinionated framework for building production-ready Spring applications with minimal configuration.
java
spring
web
rest

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