01 · Spring Ecosystem in Production¶
Level 3 introduced Spring Boot for building a REST API. Production systems need more: repositories that express real query logic without hand-written SQL, and a security layer that decides who can call what. This module covers Spring Data JPA beyond basic CRUD, and the fundamentals of Spring Security.
Spring Data JPA — derived query methods¶
Spring Data can generate queries from method names alone — no SQL, no annotations, just a naming convention it parses at startup.
// UserRepository.java
package com.example.orders.repository;
import com.example.orders.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// SELECT * FROM users WHERE email = ? AND active = true
Optional<User> findByEmailAndActiveTrue(String email);
// SELECT * FROM users WHERE last_name LIKE ?
List<User> findByLastNameStartingWithIgnoreCase(String prefix);
// SELECT COUNT(*) FROM users WHERE active = true
long countByActiveTrue();
// SELECT * FROM users WHERE created_at > ? ORDER BY created_at DESC
List<User> findByCreatedAtAfterOrderByCreatedAtDesc(java.time.Instant since);
}
Method names are parsed into predicates: findBy, And/Or, comparison
keywords (After, StartingWith, GreaterThan), and a trailing True/False
for booleans. Spring Data generates the JPQL at application startup and fails
fast if a method name doesn't map to a real property.
@Query for anything derived names can't express¶
When a query needs joins, aggregation, or is simply clearer written out, drop to JPQL (or native SQL) explicitly:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT o FROM Order o
WHERE o.customer.id = :customerId
AND o.status = 'SHIPPED'
ORDER BY o.createdAt DESC
""")
List<Order> findShippedOrdersForCustomer(@Param("customerId") Long customerId);
// Native SQL when JPQL can't express a dialect-specific feature
@Query(value = "SELECT * FROM orders WHERE total_cents > :minCents", nativeQuery = true)
List<Order> findHighValueOrders(@Param("minCents") long minCents);
}
@Param binds the named parameter in the query string to the method
argument — this avoids string concatenation entirely, so these queries are
not vulnerable to SQL injection (see
Module 5).
Pagination with Pageable¶
Returning every row is fine for a demo, not for a table with a million
orders. Accept a Pageable and return a Page<T>:
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByCustomerId(Long customerId, Pageable pageable);
}
// OrderService.java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
public Page<Order> ordersForCustomer(Long customerId, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return orderRepository.findByCustomerId(customerId, pageable);
}
Page<T> carries the content plus getTotalElements(), getTotalPages(),
and hasNext() — everything a paginated API response needs.
Spring Security basics¶
Spring Security wires a filter chain in front of every request. The modern
(non-deprecated) way to configure it is a SecurityFilterChain bean:
// SecurityConfig.java
package com.example.orders.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // salts + hashes automatically
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(basic -> {}) // HTTP Basic for simplicity here
.formLogin(form -> {}) // also enable form login
.csrf(csrf -> csrf.disable()); // safe to disable for stateless APIs using tokens
return http.build();
}
}
Never store plaintext passwords. BCryptPasswordEncoder hashes with a random
salt baked into the output, so two identical passwords produce different
hashes:
PasswordEncoder encoder = new BCryptPasswordEncoder();
String hash = encoder.encode("correct-horse-battery-staple");
// stored in the database, e.g. "$2a$10$N9qo8uLOickgx2ZMRZoMy..."
boolean matches = encoder.matches("correct-horse-battery-staple", hash); // true
boolean wrong = encoder.matches("wrong-password", hash); // false
Method-level authorization with @PreAuthorize¶
.authorizeHttpRequests covers whole URL patterns. @PreAuthorize lets you
guard an individual method with a Spring Expression Language (SpEL)
condition, evaluated before the method body runs:
// OrderController.java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public void deleteOrder(@PathVariable Long id) {
orderService.delete(id);
}
@PreAuthorize("#customerId == authentication.principal.id or hasRole('ADMIN')")
@GetMapping("/customer/{customerId}")
public List<Order> ordersForCustomer(@PathVariable Long customerId) {
return orderService.forCustomer(customerId);
}
}
@PreAuthorize requires @EnableMethodSecurity on a configuration class to
be active.
| Concept | Purpose |
|---|---|
JpaRepository<T, ID> |
Base interface giving CRUD + paging for free |
| Derived query method | Query generated from the method name |
@Query |
Explicit JPQL/native SQL for complex queries |
Pageable / Page<T> |
Request and receive one page of a large result set |
SecurityFilterChain |
Declares which requests need which authorization |
BCryptPasswordEncoder |
One-way salted password hashing |
@PreAuthorize |
Method-level, expression-based access control |
How It Actually Works¶
Spring's dependency injection container builds an in-memory graph of
BeanDefinition objects during context startup — metadata (class,
scope, dependencies, lifecycle callbacks) assembled from component
scanning, @Configuration classes, and reflection over constructors/
setters — then instantiates beans in dependency order, using
Constructor.newInstance()/reflective setter calls, wiring
@Autowired fields by matching type (and qualifier) against other bean
definitions already resolved. Circular constructor dependencies fail
hard here because the container can't construct A before B when each
needs the other fully built first; circular setter dependencies work
because Spring can hand out a not-yet-fully-populated bean reference
early and finish wiring it after both exist.
@Transactional works via a runtime proxy (JDK dynamic proxy for
interface-based beans, CGLIB-generated subclass otherwise) wrapped
around your actual bean — calling a @Transactional method from
outside the proxy triggers the transaction-management interceptor;
calling it from another method inside the same class bypasses the
proxy entirely (a direct, non-intercepted call), which is the actual
mechanical reason "self-invocation doesn't get a transaction" is a real,
frequently-hit Spring gotcha rather than folklore.
Production profiles/config (application-prod.yml) are resolved via a
layered PropertySource search at context startup, environment
variables and command-line args taking precedence over file-based
config, evaluated once and cached in the Environment abstraction.
Exercise¶
Add a findByActiveTrueAndCountryCode(String countryCode) derived query
method to a CustomerRepository, then add a paginated
findByCountryCode(String countryCode, Pageable pageable) method. Next, write
a SecurityFilterChain bean that permits GET /customers/** to any
authenticated user but restricts POST /customers/** and DELETE
/customers/** to users with role ADMIN, and add a @PreAuthorize("hasRole('ADMIN')")
annotation directly on the controller's delete method as a second line of
defense.