Post

Spring Boot Fundamentals Cheatsheet

Personal cheatsheet for Spring Boot fundamentals. Covers Maven, IoC/DI, AOP, beans, configuration, REST, JPA, security, testing, and more.

1. Project Setup

1.1. Spring Initializr

Generate a project at start.spring.io or via IntelliJ. Choose:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.x
  • Packaging: Jar (most common) or War (for external servlet container)
  • Java: 17 or 21

1.2. Group, Artifact, Package Name

FieldExampleMeaning
groupIdcom.exampleOrganisation/domain (reverse domain)
artifactIdmy-appProject name / module name
version0.0.1-SNAPSHOTBuild version. SNAPSHOT = in-development
namemy-appDisplay name
package namecom.example.myappRoot Java package for source files

1.3. JAR vs WAR

 JARWAR
ContainsApp + embedded TomcatApp only (no server)
DeploymentRun directly with java -jarDeploy to external Tomcat/JBoss
Default in Spring BootyesNeeds extends SpringBootServletInitializer

1.4. Standard Maven Project Layout

1
2
3
4
5
6
7
8
9
10
11
12
my-app/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/com/example/myapp/
    │   │   └── MyAppApplication.java
    │   └── resources/
    │       ├── application.yml
    │       └── static/      # served as-is (CSS, JS)
    │       └── templates/   # Thymeleaf etc.
    └── test/
        └── java/com/example/myapp/

2. Maven Essentials

2.1. pom.xml Anatomy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<project>
  <modelVersion>4.0.0</modelVersion>

  <!-- Project coordinates -->
  <groupId>com.example</groupId>
  <artifactId>my-app</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <!-- Inherit Spring Boot defaults -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
  </parent>

  <properties>
    <java.version>17</java.version>
  </properties>

  <dependencies>
    <!-- Starters pull in all needed transitive dependencies -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>   <!-- only on test classpath -->
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- Allows mvn spring-boot:run and creates executable fat JAR -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

2.2. Dependencies vs Plugins

 <dependencies><plugins>
PurposeLibraries your code needs at compile/runtime/testTools that participate in the build process
Examplespring-boot-starter-webspring-boot-maven-plugin
On classpath?yesno

2.3. What Is the Classpath?

List of directories and JARs the JVM searches when loading classes. Maven manages this by scope:

ScopeCompileTestRuntime JAR
compile (default)yesyesyes
testnoyesno
runtimenoyesyes
providedyesyesno (e.g. servlet API when WAR)

2.4. Maven Lifecycle Phases

Phases run sequentially - running a later phase runs all prior ones.

PhaseWhat it does
validateValidate project structure
compileCompile src/main/java
testRun unit tests (src/test/java)
packagePackage into JAR/WAR
verifyRun integration tests
installInstall artifact to local ~/.m2 repository
deployPush to remote repository

2.5. Common Commands

1
2
3
4
5
6
7
8
9
10
11
12
mvn clean                    # delete target/ directory
mvn compile                  # compile source
mvn test                     # run tests
mvn package                  # build the JAR
mvn clean install            # clean build + install to local repo
mvn clean install -DskipTests # skip tests

mvn spring-boot:run          # run app directly (no JAR needed)
java -jar target/my-app.jar  # run the built fat JAR

mvn dependency:tree          # show full dependency tree
mvn dependency:resolve       # download all dependencies

2.6. Spring Boot Starters

Each starter is a single dependency that pulls in all transitive dependencies needed for a feature.

StarterWhat it brings
spring-boot-starter-webSpring MVC, Tomcat, Jackson
spring-boot-starter-data-jpaHibernate, Spring Data JPA, JDBC
spring-boot-starter-securitySpring Security
spring-boot-starter-testJUnit 5, Mockito, MockMvc, AssertJ
spring-boot-starter-validationHibernate Validator
spring-boot-starter-actuatorHealth, metrics, info endpoints
spring-boot-starter-aopAspectJ for AOP
spring-boot-starter-mailJavaMail integration
spring-boot-starter-cacheSpring Cache abstraction

3. IoC Container & Dependency Injection

3.1. Inversion of Control (IoC)

Traditional code: you create and manage dependencies (new Service()).

IoC: the container creates and manages objects. You declare what you need, the container wires it.

3.2. ApplicationContext vs BeanFactory

 BeanFactoryApplicationContext
Basic DIyesyes
Eager bean initno (lazy)yes (by default)
Event publishingnoyes
AOP integrationnoyes
i18n, MessageSourcenoyes
Use in Spring BootNever directlySpringApplication.run() returns this
1
2
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
MyService svc = ctx.getBean(MyService.class); // manual retrieval - rarely needed

3.3. Types of Dependency Injection

Constructor Injection (preferred):

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) { // Spring injects this
        this.paymentService = paymentService;
    }
}
// If only one constructor exists, @Autowired is optional (Spring Boot auto-detects)

Setter Injection (optional dependencies):

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

Field Injection (avoid - hard to test, hides dependencies):

1
2
3
4
5
@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService; // works but discouraged
}

4. Configuration Classes in Depth

4.1. @Configuration and @Bean

@Configuration marks a class as a source of bean definitions. Methods annotated with @Bean are factory methods - the return value becomes a Spring-managed bean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
        ds.setUsername("user");
        ds.setPassword("pass");
        return ds;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) { // Spring injects the bean above
        return new JdbcTemplate(dataSource);
    }
}

@Configuration classes are CGLIB-proxied by default - calling dataSource() from within the config class returns the same singleton bean, not a new instance each time.

4.2. @Import

Include another configuration class without component scanning:

1
2
3
@Configuration
@Import({DatabaseConfig.class, SecurityConfig.class})
public class AppConfig { ... }

4.3. @PropertySource

Load additional .properties files into the Environment:

1
2
3
4
5
6
@Configuration
@PropertySource("classpath:custom.properties")
public class AppConfig {
    @Value("${custom.timeout}")
    private int timeout;
}

4.4. Conditional Beans

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Only create bean if a condition is met
@Bean
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
public CacheManager cacheManager() { return new ConcurrentMapCacheManager(); }

@Bean
@ConditionalOnMissingBean(DataSource.class)  // only if no DataSource bean exists
public DataSource defaultDataSource() { ... }

@Bean
@ConditionalOnClass(name = "com.example.SomeLibrary") // only if class on classpath
public SomeLibraryIntegration integration() { ... }

@Bean
@Profile("prod") // only active when 'prod' profile is active
public EmailSender realEmailSender() { ... }

@Bean
@Profile("dev")
public EmailSender fakeEmailSender() { ... }

4.5. @EnableXxx Annotations

Used on @Configuration classes to activate specific Spring features:

AnnotationEffect
@EnableAutoConfigurationTurns on Spring Boot auto-config (included in @SpringBootApplication)
@EnableWebMvcFull control over MVC config (disables Spring Boot MVC auto-config)
@EnableJpaRepositoriesEnable Spring Data JPA repo scanning
@EnableTransactionManagementEnable @Transactional support
@EnableAsyncEnable @Async method execution
@EnableSchedulingEnable @Scheduled tasks
@EnableCachingEnable Spring’s cache abstraction
@EnableAspectJAutoProxyEnable AOP via AspectJ

5. Auto-Configuration

5.1. How It Works

@SpringBootApplication includes @EnableAutoConfiguration, which triggers the auto-configuration mechanism:

  1. Spring Boot reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from all JARs on the classpath
  2. Each entry is an @AutoConfiguration class gated by @Conditional annotations
  3. If the conditions are met (e.g. a class is on the classpath, a property is set), the beans are registered
  4. Your own beans take priority - auto-configured beans only register if you haven’t defined one
1
2
3
4
5
6
7
// Example: DataSourceAutoConfiguration only activates if:
// - spring-jdbc is on the classpath
// - No DataSource bean is already defined
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration { ... }

5.2. Debugging Auto-Configuration

1
2
3
4
5
# Add to application.yml to see what was configured and why
logging.level.org.springframework.boot.autoconfigure=DEBUG

# Or run with --debug flag
java -jar app.jar --debug

The “Conditions Evaluation Report” on startup shows every auto-configuration class and whether it was applied or skipped.


6. Beans & Stereotypes

6.1. Stereotype Annotations

All are specialisations of @Component - picked up by component scanning:

AnnotationLayerExtra behaviour
@ComponentGenericBase stereotype
@ServiceBusiness logicNone (semantic only)
@RepositoryData accessWraps persistence exceptions into DataAccessException
@ControllerWeb MVCHandles HTTP, returns view name
@RestControllerWeb REST@Controller + @ResponseBody on all methods
@ConfigurationConfigCGLIB proxy for @Bean methods

6.2. Component Scanning

@SpringBootApplication includes @ComponentScan, which scans the main class package and all sub-packages. Any @Component class (or stereotype) in those packages gets registered as a bean.

1
2
// If you need to scan additional packages:
@ComponentScan(basePackages = {"com.example.app", "com.example.shared"})

6.3. Key DI Annotations

AnnotationPurpose
@AutowiredInject a dependency by type
@Qualifier("beanName")Disambiguate when multiple beans of same type exist
@PrimaryMark one bean as default when multiple exist
@Value("${prop.key}")Inject a property value
@LazyDelay bean initialisation until first use
@DependsOn("otherBean")Force another bean to be initialised first
1
2
3
4
5
6
7
8
9
10
11
12
13
// Multiple implementations of same interface
@Service @Primary
public class EmailNotificationService implements NotificationService { ... }

@Service @Qualifier("sms")
public class SmsNotificationService implements NotificationService { ... }

// Injection
@Autowired
private NotificationService notificationService; // gets EmailNotificationService (Primary)

@Autowired @Qualifier("sms")
private NotificationService smsService;          // gets SmsNotificationService explicitly

7. Bean Lifecycle

1
2
3
4
5
6
1. Instantiation          - Spring creates the object (calls constructor)
2. Dependency Injection   - Spring injects all dependencies (@Autowired, @Value, etc.)
3. @PostConstruct         - Your initialisation logic (runs after DI is complete)
4. In Use                 - Bean serves requests
5. @PreDestroy            - Your cleanup logic (runs before bean is destroyed)
6. Destruction            - Bean is removed from context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class CacheService {

    private Map<String, Object> cache;

    @PostConstruct
    public void init() {
        cache = new HashMap<>();
        System.out.println("Cache initialized");
    }

    @PreDestroy
    public void cleanup() {
        cache.clear();
        System.out.println("Cache cleared");
    }
}

Alternatively, use InitializingBean / DisposableBean interfaces, or specify initMethod/destroyMethod in @Bean:

1
2
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() { ... }

8. Bean Scopes

ScopeOne instance per…Default
singletonSpring container (entire app)yes
prototypeEach request for the bean (getBean() call)no
requestHTTP request (web-aware contexts only)no
sessionHTTP session (web-aware contexts only)no
applicationServletContext (one per app)no
1
2
3
@Component
@Scope("prototype") // or use ConfigurableBeanFactory.SCOPE_PROTOTYPE constant
public class ReportGenerator { ... }

Singleton + Prototype injection problem: If a singleton bean needs a new prototype instance per method call, inject ApplicationContext or use @Lookup:

1
2
3
4
5
6
7
8
9
@Component
public class OrderProcessor {
    @Autowired
    private ApplicationContext ctx;

    public void process() {
        ReportGenerator gen = ctx.getBean(ReportGenerator.class); // new instance each time
    }
}

9. AOP (Aspect-Oriented Programming)

9.1. What It Is

Separates cross-cutting concerns (logging, security, transactions, metrics) from business logic. Define the logic once in an Aspect, apply it to many methods via pointcuts.

Key concepts:

  • Aspect - the module containing cross-cutting logic
  • Advice - the code to run (@Before, @After, @Around, etc.)
  • Pointcut - expression that selects which methods to intercept
  • Join point - specific point in execution (method call, in Spring AOP)
  • Weaving - linking aspects to target code (Spring does this at runtime via proxies)

9.2. Dependency

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

9.3. Advice Types

Advice is the code that executes at a join point. Spring AOP provides five types: @Before, @AfterReturning, @AfterThrowing, @After (finally), and @Around.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Aspect
@Component
public class LoggingAspect {

    // Runs before the matched method
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint jp) {
        System.out.println("Calling: " + jp.getSignature().getName());
    }

    // Runs after method returns (not on exception)
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Returned: " + result);
    }

    // Runs after exception is thrown
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void logAfterThrowing(Exception ex) {
        System.err.println("Exception: " + ex.getMessage());
    }

    // Runs after method regardless (like finally)
    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint jp) { ... }

    // Wraps the method - most powerful
    @Around("execution(* com.example.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // call the actual method
        long elapsed = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature().getName() + " took " + elapsed + "ms");
        return result;
    }
}

9.4. Pointcut Expressions

Pointcut expressions use AspectJ syntax to select which methods advice applies to. Named pointcuts (@Pointcut) can be reused across multiple advice methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// All methods in a package
execution(* com.example.service.*.*(..))

// Specific method
execution(public String com.example.service.UserService.findById(Long))

// Methods with a specific annotation
@annotation(org.springframework.transaction.annotation.Transactional)

// All public methods
execution(public * *(..))

// Combined
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceLayer() {}

@Before("serviceLayer()")
public void logService(JoinPoint jp) { ... }

9.5. Limitations

Spring AOP only works on Spring-managed beans and only intercepts external method calls through the proxy. A method calling another method on the same bean bypasses the proxy - the advice won’t run.


10. Properties & YAML Configuration

10.1. application.yml vs application.properties

Both are equivalent - YAML is more readable for hierarchical config.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# application.yml
server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: secret
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

app:
  feature:
    cache-enabled: true
  max-upload-size: 10MB
1
2
3
# application.properties equivalent
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb

10.2. Injecting Properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Single value
@Value("${app.max-upload-size}")
private String maxUploadSize;

@Value("${app.feature.cache-enabled:false}") // with default
private boolean cacheEnabled;

// Whole group of properties - type-safe
@ConfigurationProperties(prefix = "app.feature")
@Component // or used with @EnableConfigurationProperties on a @Configuration class
public class FeatureProperties {
    private boolean cacheEnabled;  // maps to app.feature.cache-enabled
    private int retryCount;        // maps to app.feature.retry-count
    // getters and setters required (or use record)
}
1
2
3
4
@Autowired
private FeatureProperties featureProps;

featureProps.isCacheEnabled();

10.3. Profiles

Activate different config per environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# application.yml - shared base config
app:
  name: MyApp

---
# Profile-specific section (Spring Boot 2.4+)
spring:
  config:
    activate:
      on-profile: dev
spring.datasource.url: jdbc:h2:mem:testdb

---
spring:
  config:
    activate:
      on-profile: prod
spring.datasource.url: jdbc:postgresql://prod-db:5432/mydb

Or use separate files: application-dev.yml, application-prod.yml

1
2
3
4
# Activate a profile
java -jar app.jar --spring.profiles.active=prod
# Or in YAML
spring.profiles.active: dev

10.4. Externalising Config

Spring Boot loads config in this priority order (higher = wins):

  1. Command-line args (--server.port=9090)
  2. OS environment variables (SERVER_PORT=9090)
  3. application.yml / application.properties in /config dir next to JAR
  4. application.yml / application.properties in classpath root
  5. @PropertySource files
  6. @Value defaults

11. Building REST Endpoints

11.1. Basic Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@RestController // = @Controller + @ResponseBody on all methods
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping                         // GET /api/users
    public List<UserDto> getAll() {
        return userService.findAll();
    }

    @GetMapping("/{id}")                // GET /api/users/123
    public ResponseEntity<UserDto> getById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping                        // POST /api/users
    public ResponseEntity<UserDto> create(@RequestBody @Valid CreateUserRequest request) {
        UserDto created = userService.create(request);
        URI location = URI.create("/api/users/" + created.getId());
        return ResponseEntity.created(location).body(created); // 201 + Location header
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserDto> update(@PathVariable Long id,
                                          @RequestBody @Valid UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT) // 204
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

11.2. Request Binding Annotations

AnnotationBinds fromExample
@PathVariableURL path segmentGET /users/{id}
@RequestParamQuery stringGET /users?page=1&size=10
@RequestBodyRequest body (JSON → object)POST /users with JSON body
@RequestHeaderHTTP headerAuthorization header
@CookieValueCookiesession cookie
1
2
3
4
5
@GetMapping("/search")
public List<UserDto> search(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size,
    @RequestParam(required = false) String name) { ... }

11.3. ResponseEntity

Full control over HTTP response - status code, headers, body:

1
2
3
4
5
6
7
8
9
10
return ResponseEntity.ok(body);                         // 200
return ResponseEntity.created(location).body(dto);      // 201
return ResponseEntity.noContent().build();              // 204
return ResponseEntity.notFound().build();               // 404
return ResponseEntity.status(HttpStatus.CONFLICT).body(error); // 409

// With custom headers
return ResponseEntity.ok()
    .header("X-Custom-Header", "value")
    .body(dto);

11.4. CORS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Per controller or method
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class UserController { ... }

// Global - in a @Configuration class
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000", "https://myapp.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

11.5. Filters vs Interceptors

 OncePerRequestFilter (Filter)HandlerInterceptor
Runs atServlet level - before Spring MVCSpring MVC level - after DispatcherServlet
Access to Spring beansyes (if a Spring component)yes
Can abort requestyes (filterChain.doFilter not called)yes (preHandle returns false)
Sees response bodyyesOnly afterCompletion (already committed)
Best forAuth, logging, encoding, rate limitingLogging, auth checking (Spring MVC level), locale
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Filter
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        System.out.println("Request: " + req.getMethod() + " " + req.getRequestURI());
        chain.doFilter(req, res); // pass to next filter/servlet
        System.out.println("Response status: " + res.getStatus());
    }
}

// Interceptor - must register in WebMvcConfigurer
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        String token = req.getHeader("Authorization");
        if (token == null) {
            res.setStatus(401);
            return false; // abort
        }
        return true; // continue
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**");
    }
}

12. Bean Validation

Add the validation starter to enable @Valid/@Validated support and the Hibernate Validator constraint annotations.

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

12.1. Constraint Annotations

Annotate fields with constraint annotations. Use @Valid on nested objects to cascade validation into them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CreateUserRequest {
    @NotNull
    @NotBlank
    @Size(min = 2, max = 50)
    private String name;

    @Email
    private String email;

    @Min(0) @Max(150)
    private int age;

    @Pattern(regexp = "^\\+?[0-9]{8,15}$")
    private String phone;

    @NotNull
    @Future  // date must be in the future
    private LocalDate appointmentDate;

    @Valid   // cascade validation to nested object
    private AddressRequest address;
}

12.2. Triggering Validation

Place @Valid on the @RequestBody parameter. Constraint violations automatically produce a 400 response, handled by MethodArgumentNotValidException.

1
2
3
4
5
6
// @Valid on controller parameter triggers validation
@PostMapping
public ResponseEntity<UserDto> create(@RequestBody @Valid CreateUserRequest req) { ... }

// Validation errors automatically return 400 Bad Request
// To customise the error response, handle MethodArgumentNotValidException in @ControllerAdvice

12.3. @Validated vs @Valid

 @Valid@Validated
Originjavax.validation / jakarta.validationSpring
Supports groupsnoyes
Use onController params, nested objectsClass-level (service), groups
1
2
3
4
5
6
7
8
9
10
11
12
// Validation groups for different operations
public interface OnCreate {}
public interface OnUpdate {}

public class UserRequest {
    @Null(groups = OnCreate.class)    // id must be null on create
    @NotNull(groups = OnUpdate.class) // id must be present on update
    private Long id;
}

@PutMapping("/{id}")
public UserDto update(@RequestBody @Validated(OnUpdate.class) UserRequest req) { ... }

13. Exception Handling

13.1. @ControllerAdvice + @ExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@ControllerAdvice
public class GlobalExceptionHandler {

    // Handle specific exception
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex,
                                                         HttpServletRequest req) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            req.getRequestURI()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    // Handle validation failures
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationError(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.toList());
        ErrorResponse error = new ErrorResponse(400, "Validation failed", errors);
        return ResponseEntity.badRequest().body(error);
    }

    // Catch-all
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        return ResponseEntity.internalServerError()
            .body(new ErrorResponse(500, "Internal server error"));
    }
}

13.2. RFC 7807 ProblemDetail (Spring Boot 3.x)

Spring Boot 3 has built-in support for RFC 7807 Problem Details:

1
2
3
4
5
6
7
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setTitle("Resource Not Found");
    pd.setProperty("timestamp", Instant.now());
    return pd;
}
1
2
3
4
5
# Enable automatic ProblemDetail for Spring MVC exceptions
spring:
  mvc:
    problemdetails:
      enabled: true

14. @Async and @Scheduled

14.1. @Async

Enable async with @EnableAsync on a config class. Annotate methods with @Async - they run in a separate thread and return immediately.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class EmailService {

    @Async
    public void sendEmail(String to, String body) {
        // runs in background thread
        // caller gets control back immediately
    }

    @Async
    public CompletableFuture<String> fetchData() {
        String result = doSlowWork();
        return CompletableFuture.completedFuture(result);
    }
}

Caveat: Same self-invocation problem as AOP - calling an @Async method from within the same bean won’t be async (proxy is bypassed).

14.2. @Scheduled

Enable with @EnableScheduling on a config class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableScheduling
public class SchedulingConfig {}

@Component
public class ReportTask {

    @Scheduled(fixedDelay = 5000)   // 5s after last run finishes
    public void runReport() { ... }

    @Scheduled(fixedRate = 60000)   // every 60s, regardless of duration
    public void syncData() { ... }

    @Scheduled(initialDelay = 10000, fixedRate = 60000) // wait 10s before first run
    public void delayedTask() { ... }

    @Scheduled(cron = "0 0 9 * * MON-FRI") // every weekday at 9am
    public void morningTask() { ... }
    // cron: second minute hour day-of-month month day-of-week
}

15. Spring Data JPA

15.1. Entity Mapping

Annotate a class with @Entity to map it to a database table. Use JPA annotations to control column names, nullability, relationships, and ID generation strategy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment
    private Long id;

    @Column(name = "full_name", nullable = false, length = 100)
    private String name;

    @Column(unique = true)
    private String email;

    @Enumerated(EnumType.STRING) // store enum name, not ordinal
    private Role role;

    @CreatedDate  // auto-populated by Spring Data auditing
    private LocalDateTime createdAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
}

15.2. Relationships

AnnotationMeaningDefault Fetch
@OneToOneOne-to-oneEAGER
@OneToManyOne entity has manyLAZY
@ManyToOneMany entities belong to oneEAGER
@ManyToManyMany-to-many (join table)LAZY

Always prefer LAZY fetching. EAGER can cause N+1 problems and load unnecessary data.

1
2
3
4
5
6
7
8
9
// @ManyToMany
@Entity
public class Student {
    @ManyToMany
    @JoinTable(name = "student_courses",
               joinColumns = @JoinColumn(name = "student_id"),
               inverseJoinColumns = @JoinColumn(name = "course_id"))
    private Set<Course> courses = new HashSet<>();
}

15.3. JpaRepository

JpaRepository provides CRUD, paging, and sorting out of the box. Spring generates the implementation at startup from method names or @Query annotations.

1
2
3
4
5
6
7
8
9
10
11
public interface UserRepository extends JpaRepository<User, Long> {
    // JpaRepository provides: save, findById, findAll, deleteById, count, existsById, etc.

    // Derived query methods - Spring generates SQL from method name
    Optional<User> findByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);
    List<User> findByAgeGreaterThanAndRoleEquals(int age, Role role);
    boolean existsByEmail(String email);
    long countByRole(Role role);
    List<User> findAllByOrderByNameAsc();
}

15.4. @Query

Use JPQL to reference entity class and field names. For native SQL, set nativeQuery = true. Modifying queries require @Modifying and @Transactional.

1
2
3
4
5
6
7
8
9
10
11
12
13
// JPQL - uses entity class and field names, not table/column names
@Query("SELECT u FROM User u WHERE u.email = :email AND u.role = :role")
Optional<User> findByEmailAndRole(@Param("email") String email, @Param("role") Role role);

// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > :since", nativeQuery = true)
List<User> findRecentUsers(@Param("since") LocalDateTime since);

// Modifying query - for UPDATE/DELETE
@Modifying
@Transactional
@Query("UPDATE User u SET u.role = :role WHERE u.id = :id")
int updateRole(@Param("id") Long id, @Param("role") Role role);

15.5. @Transactional - Propagation & Isolation

Propagation - how transactions relate to each other when methods call each other:

PropagationBehaviour
REQUIRED (default)Join existing transaction; create new if none
REQUIRES_NEWAlways create new transaction; suspend existing
MANDATORYMust have existing transaction; throw if none
NEVERMust NOT have transaction; throw if one exists
NOT_SUPPORTEDSuspend existing transaction; run without
SUPPORTSJoin if exists; run without if none
NESTEDNested savepoint within existing transaction

Isolation - how visible other transactions’ changes are:

IsolationDirty ReadNon-repeatable ReadPhantom Read
READ_UNCOMMITTEDyesyesyes
READ_COMMITTEDnoyesyes
REPEATABLE_READnonoyes
SERIALIZABLEnonono
1
2
3
4
5
6
7
8
9
10
11
12
@Transactional(
    propagation = Propagation.REQUIRES_NEW,
    isolation = Isolation.READ_COMMITTED,
    rollbackFor = Exception.class,        // default: only RuntimeException
    timeout = 30                          // seconds
)
public void processPayment(Long orderId) {
    // runs in its own new transaction
}

@Transactional(readOnly = true) // optimisation hint for SELECT-only methods
public List<User> findAll() { ... }

Self-invocation caveat: Calling a @Transactional method from within the same bean bypasses the proxy - the transaction annotation is ignored.


16. Spring Security (Basics)

Adding this dependency immediately secures all endpoints with basic auth. The default user is user and the password is printed in the console on startup.

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

16.1. SecurityFilterChain

SecurityFilterChain defines the HTTP security rules applied to every request — which paths require authentication, what roles are needed, session policy, and CSRF settings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())          // disable for stateless APIs
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults()); // or .formLogin() or JWT filter

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

16.2. Common Annotations

1
2
3
4
5
6
@PreAuthorize("hasRole('ADMIN')")                    // on method
@PreAuthorize("hasAuthority('user:read')")
@PreAuthorize("#id == authentication.principal.id")  // SpEL expression

// Enable method security on config class
@EnableMethodSecurity

17. Startup Hooks

Run initialization logic after the application context is ready by implementing CommandLineRunner or ApplicationRunner. Both are called with any command-line arguments passed to the app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Runs after application context is ready
@Component
public class DataLoader implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        // seed data, warmup caches, etc.
        System.out.println("Application started with args: " + Arrays.toString(args));
    }
}

// ApplicationRunner - same but receives ApplicationArguments (more structured)
@Component
public class AppRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        boolean debug = args.containsOption("debug");
        List<String> files = args.getNonOptionArgs();
    }
}

18. Actuator

Add the actuator starter to expose health, metrics, and operational endpoints. Configure which endpoints are exposed via management.endpoints.web.exposure.include.

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4
5
6
7
8
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env,loggers  # or "*" for all
  endpoint:
    health:
      show-details: always
EndpointURLDescription
HealthGET /actuator/healthApp health status
InfoGET /actuator/infoCustom app info
MetricsGET /actuator/metricsJVM, HTTP, custom metrics
EnvGET /actuator/envAll properties
LoggersGET/POST /actuator/loggersView/change log levels at runtime
BeansGET /actuator/beansAll beans in context
MappingsGET /actuator/mappingsAll @RequestMapping routes

19. Testing

19.1. Test Slice Annotations

AnnotationLoadsUse for
@SpringBootTestFull application contextIntegration tests
@WebMvcTest(Controller.class)Only web layer (controller + MVC)Controller unit tests
@DataJpaTestJPA layer + H2 in-memory DBRepository tests
@RestClientTestRestTemplate/RestClient componentsREST client tests

19.2. @SpringBootTest

Loads the full application context. WebEnvironment.MOCK (default) uses MockMvc without a real HTTP port. WebEnvironment.RANDOM_PORT starts a real embedded server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void createUser_returns201() {
        CreateUserRequest req = new CreateUserRequest("Ryo", "ryo@example.com");
        ResponseEntity<UserDto> res = restTemplate.postForEntity("/api/users", req, UserDto.class);

        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(res.getBody().getName()).isEqualTo("Ryo");
    }
}

19.3. @WebMvcTest + MockMvc

Only loads the web layer. Any Spring beans the controller depends on must be mocked with @MockBean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void getUser_returns200() throws Exception {
        UserDto dto = new UserDto(1L, "Ryo", "ryo@example.com");
        given(userService.findById(1L)).willReturn(Optional.of(dto));

        mockMvc.perform(get("/api/users/1")
                    .contentType(MediaType.APPLICATION_JSON))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name").value("Ryo"))
               .andExpect(jsonPath("$.email").value("ryo@example.com"));
    }

    @Test
    void createUser_withInvalidBody_returns400() throws Exception {
        CreateUserRequest req = new CreateUserRequest("", "not-an-email"); // invalid

        mockMvc.perform(post("/api/users")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(req)))
               .andExpect(status().isBadRequest());
    }

    @Test
    void createUser_returns201() throws Exception {
        CreateUserRequest req = new CreateUserRequest("Ryo", "ryo@example.com");
        UserDto dto = new UserDto(1L, "Ryo", "ryo@example.com");
        given(userService.create(any(CreateUserRequest.class))).willReturn(dto);

        mockMvc.perform(post("/api/users")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(req)))
               .andExpect(status().isCreated())
               .andExpect(header().exists("Location"))
               .andExpect(jsonPath("$.id").value(1L));
    }
}

19.4. @DataJpaTest

In-memory H2 database. Only loads JPA-related beans. Transactions roll back after each test by default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager; // helper for setting up test data

    @Test
    void findByEmail_returnsUser() {
        User user = new User("Ryo", "ryo@example.com");
        entityManager.persistAndFlush(user); // persist and flush to DB

        Optional<User> found = userRepository.findByEmail("ryo@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Ryo");
    }

    @Test
    void findByEmail_notFound_returnsEmpty() {
        Optional<User> found = userRepository.findByEmail("none@example.com");
        assertThat(found).isEmpty();
    }
}

To use a real database instead of H2:

1
2
3
4
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // use actual DB
@ActiveProfiles("test")
class UserRepositoryTest { ... }

19.5. Mockito in Depth

Included in spring-boot-starter-test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private PaymentService paymentService;   // full mock - all methods return default values

    @Mock
    private OrderRepository orderRepository;

    @Spy
    private PricingEngine pricingEngine = new PricingEngine(); // real object, can spy on calls

    @Captor
    private ArgumentCaptor<Order> orderCaptor; // capture args passed to mock

    @InjectMocks
    private OrderService orderService; // creates instance and injects mocks above

    @Test
    void processOrder_callsPaymentWithCorrectAmount() {
        Order order = new Order(100.0);
        given(orderRepository.save(any(Order.class))).willReturn(order); // BDDMockito style
        when(paymentService.charge(anyDouble())).thenReturn(true);       // Mockito style

        orderService.process(order);

        // Verify a method was called
        verify(paymentService).charge(100.0);

        // Verify with argument captor
        verify(orderRepository).save(orderCaptor.capture());
        Order savedOrder = orderCaptor.getValue();
        assertThat(savedOrder.getStatus()).isEqualTo(OrderStatus.PROCESSED);

        // Verify call count
        verify(paymentService, times(1)).charge(anyDouble());
        verify(orderRepository, never()).delete(any());
    }

    @Test
    void processOrder_paymentFails_throwsException() {
        when(paymentService.charge(anyDouble())).thenThrow(new PaymentException("declined"));

        assertThatThrownBy(() -> orderService.process(new Order(50.0)))
            .isInstanceOf(PaymentException.class)
            .hasMessage("declined");
    }

    @Test
    void spyExample() {
        doReturn(99.0).when(pricingEngine).calculate(any()); // override only this method
        // rest of pricingEngine runs real code
    }
}

Key Mockito methods:

MethodPurpose
when(mock.method()).thenReturn(value)Stub a return value
when(mock.method()).thenThrow(ex)Stub an exception
given(mock.method()).willReturn(value)BDDMockito style (preferred)
doReturn(val).when(spy).method()Stub on spy (avoids calling real method during stubbing)
verify(mock).method(args)Assert method was called
verify(mock, times(n)).method(args)Assert exact call count
verify(mock, never()).method(args)Assert never called
verify(mock, atLeast(n)).method(args)Assert called at least n times
ArgumentCaptor.capture()Capture argument for assertion
any(), anyString(), eq(val)Argument matchers
ArgumentMatchers.argThat(pred)Custom argument matcher

19.6. Testing with Profiles & Properties

Activate a profile or override specific properties for a test class without touching the main configuration.

1
2
3
4
5
6
7
@SpringBootTest
@ActiveProfiles("test")  // activate application-test.yml
class MyTest { ... }

@SpringBootTest
@TestPropertySource(properties = {"app.feature.enabled=true", "server.port=0"})
class MyTest { ... }

19.7. AssertJ (Fluent Assertions)

Included via spring-boot-starter-test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Basic
assertThat(result).isEqualTo(expected);
assertThat(list).hasSize(3).contains("a", "b");
assertThat(optional).isPresent().contains("value");
assertThat(string).startsWith("hello").endsWith("world").contains("ello");
assertThat(number).isGreaterThan(0).isLessThanOrEqualTo(100);

// Collections
assertThat(list).containsExactly("a", "b", "c");          // exact order
assertThat(list).containsExactlyInAnyOrder("c", "a", "b"); // any order
assertThat(list).filteredOn(u -> u.getAge() > 18).hasSize(2);
assertThat(list).extracting(User::getName).containsOnly("Ryo", "Alice");

// Exceptions
assertThatThrownBy(() -> service.doThing())
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("invalid");

assertThatNoException().isThrownBy(() -> service.doThing());

Comments powered by Disqus.