Spring Boot - Mikroserwisy w Javie od podstaw do produkcji
Spring Boot Mikroserwisy
backendSpring Boot: Mikroserwisy w Javie od podstaw do produkcji
Spring Boot zrewolucjonizował sposób tworzenia aplikacji w Javie. Eliminując żmudną konfigurację XML i dostarczając gotowe do użycia komponenty, pozwala programistom skupić się na logice biznesowej zamiast na infrastrukturze. W tym artykule przeprowadzimy Cię przez cały ekosystem Spring Boot — od pierwszego endpointu REST, przez bezpieczeństwo i dostęp do danych, aż po wdrożenie mikroserwisów w Kubernetes.
Czym jest Spring Boot i ekosystem Spring?#
Spring Framework to jeden z najpopularniejszych frameworków do budowy aplikacji enterprise w Javie. Powstał jako alternatywa dla ciężkiego modelu EJB i opiera się na dwóch filarach: Inversion of Control (IoC) oraz Dependency Injection (DI).
Spring Boot to nadbudowa nad Spring Framework, która wprowadza:
- Auto-konfigurację — automatyczne wykrywanie i konfigurowanie komponentów
- Embedded serwery — Tomcat, Jetty lub Undertow wbudowane w aplikację
- Spring Boot Starters — gotowe zestawy zależności dla typowych scenariuszy
- Spring Boot Actuator — monitoring i zarządzanie aplikacją
- Spring Boot DevTools — automatyczny restart i hot-reload w trybie development
Ekosystem Spring obejmuje dziesiątki projektów, ale najważniejsze to:
- Spring MVC — kontrolery REST i obsługa HTTP
- Spring Data — uproszczony dostęp do baz danych
- Spring Security — uwierzytelnianie i autoryzacja
- Spring Cloud — narzędzia do budowy mikroserwisów
- Spring Batch — przetwarzanie wsadowe
Auto-konfiguracja i Convention over Configuration#
Jednym z kluczowych mechanizmów Spring Boot jest auto-konfiguracja. Wystarczy dodać odpowiedni starter do pom.xml lub build.gradle, a Spring Boot automatycznie skonfiguruje wszystkie potrzebne beany.
// To jest wszystko, czego potrzebujesz, aby uruchomić aplikację Spring Boot
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
Adnotacja @SpringBootApplication łączy trzy mechanizmy:
@Configuration— oznacza klasę jako źródło definicji beanów@EnableAutoConfiguration— włącza auto-konfigurację@ComponentScan— skanuje pakiety w poszukiwaniu komponentów
Konfiguracja aplikacji odbywa się przez plik application.yml:
server:
port: 8080
spring:
application:
name: order-service
datasource:
url: jdbc:postgresql://localhost:5432/orders
username: ${DB_USERNAME:admin}
password: ${DB_PASSWORD:secret}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 20
Spring Data JPA — dostęp do bazy danych#
Spring Data JPA eliminuje potrzebę pisania boilerplate'u dla operacji CRUD. Wystarczy zdefiniować interfejs repozytorium, a Spring wygeneruje implementację automatycznie.
Encja JPA#
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String customerEmail;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Column(nullable = false)
private BigDecimal totalAmount;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// konstruktory, gettery, settery
}
Repozytorium#
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerEmailOrderByCreatedAtDesc(String email);
@Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since")
Page<Order> findRecentByStatus(
@Param("status") OrderStatus status,
@Param("since") LocalDateTime since,
Pageable pageable
);
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
@Query("""
SELECT new com.example.dto.OrderSummary(
o.status, COUNT(o), SUM(o.totalAmount)
)
FROM Order o
WHERE o.createdAt BETWEEN :from AND :to
GROUP BY o.status
""")
List<OrderSummary> getOrderSummary(
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to
);
}
Spring Data automatycznie generuje zapytania SQL na podstawie nazw metod (findByCustomerEmailOrderByCreatedAtDesc), a dla bardziej złożonych przypadków pozwala na definiowanie zapytań JPQL za pomocą adnotacji @Query.
REST Controllers i projektowanie API#
Spring MVC zapewnia elegancki model tworzenia kontrolerów REST. Ważne jest stosowanie dobrych praktyk projektowania API: odpowiednie kody HTTP, walidacja danych wejściowych i spójne odpowiedzi błędów.
@RestController
@RequestMapping("/api/v1/orders")
@Validated
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping
public ResponseEntity<Page<OrderResponse>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) OrderStatus status) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<OrderResponse> orders = orderService.getOrders(status, pageable);
return ResponseEntity.ok(orders);
}
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrderById(id));
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderResponse created = orderService.createOrder(request);
URI location = URI.create("/api/v1/orders/" + created.id());
return ResponseEntity.created(location).body(created);
}
@PatchMapping("/{id}/status")
public ResponseEntity<OrderResponse> updateStatus(
@PathVariable Long id,
@Valid @RequestBody UpdateStatusRequest request) {
return ResponseEntity.ok(orderService.updateStatus(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void cancelOrder(@PathVariable Long id) {
orderService.cancelOrder(id);
}
}
Globalna obsługa błędów#
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(a, b) -> a
));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
LocalDateTime.now(),
fieldErrors
);
return ResponseEntity.badRequest().body(error);
}
}
Spring Security — JWT i OAuth2#
Spring Security to kompleksowy framework zabezpieczeń. Nowoczesne aplikacje mikroserwisowe najczęściej korzystają z tokenów JWT lub protokołu OAuth2.
Konfiguracja bezpieczeństwa z JWT#
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Filtr JWT#
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Spring Cloud — infrastruktura mikroserwisów#
Spring Cloud dostarcza zestaw narzędzi niezbędnych do budowy rozproszonych systemów mikroserwisowych. Kluczowe komponenty to Service Discovery, Config Server i API Gateway.
Eureka — Service Discovery#
Eureka pozwala mikroserwisem rejestrować się i odnajdywać nawzajem bez hardkodowania adresów.
Serwer Eureka:
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
# application.yml serwera Eureka
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
Klient (mikroserwis):
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
Spring Cloud Config Server#
Centralny serwer konfiguracji pozwala na zarządzanie konfiguracją wszystkich mikroserwisów z jednego miejsca (np. repozytorium Git):
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
spring:
cloud:
config:
server:
git:
uri: https://github.com/my-org/config-repo
default-label: main
search-paths: '{application}'
Spring Cloud Gateway#
Gateway pełni rolę centralnego punktu wejścia do systemu mikroserwisów, obsługując routing, load balancing i filtrowanie żądań:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/v1/orders/**
filters:
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/orders
- name: RequestRateLimiter
args:
redis-rate-limiter:
replenishRate: 10
burstCapacity: 20
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/v1/products/**
Spring Boot Actuator — monitoring i zarządzanie#
Actuator udostępnia szereg endpointów do monitorowania stanu aplikacji, co jest niezbędne w środowisku produkcyjnym.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
probes:
enabled: true
metrics:
tags:
application: ${spring.application.name}
Niestandardowy Health Indicator#
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
public ExternalApiHealthIndicator(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public Health health() {
try {
ResponseEntity<String> response =
restTemplate.getForEntity("https://api.external-service.com/health", String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("externalApi", "Available")
.withDetail("responseTime", "OK")
.build();
}
return Health.down().withDetail("externalApi", "Unhealthy").build();
} catch (Exception e) {
return Health.down()
.withDetail("externalApi", "Unavailable")
.withException(e)
.build();
}
}
}
Spring Profiles — zarządzanie środowiskami#
Profile pozwalają na definiowanie różnych konfiguracji dla różnych środowisk: development, staging, production.
# application.yml — konfiguracja wspólna
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example: DEBUG
org.hibernate.SQL: DEBUG
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
logging:
level:
com.example: WARN
Aktywacja profilu odbywa się przez zmienną środowiskową lub parametr uruchomienia:
java -jar app.jar --spring.profiles.active=prod
# lub
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
Testowanie — MockMvc i Testcontainers#
Spring Boot oferuje bogaty ekosystem testowy. Testy integracyjne z MockMvc pozwalają testować kontrolery bez uruchamiania serwera HTTP, a Testcontainers umożliwiają testowanie z prawdziwą bazą danych w Dockerze.
Testy kontrolerów z MockMvc#
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldCreateOrder() throws Exception {
CreateOrderRequest request = new CreateOrderRequest(
"customer@example.com",
List.of(new OrderItemRequest(1L, 2))
);
OrderResponse response = new OrderResponse(
1L, "customer@example.com", OrderStatus.CREATED,
new BigDecimal("199.98"), LocalDateTime.now()
);
when(orderService.createOrder(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.status").value("CREATED"))
.andExpect(header().exists("Location"));
}
@Test
void shouldReturn400ForInvalidRequest() throws Exception {
CreateOrderRequest request = new CreateOrderRequest("", List.of());
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.fieldErrors").exists());
}
}
Testy integracyjne z Testcontainers#
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
}
@Test
void shouldCreateAndRetrieveOrder() {
CreateOrderRequest request = new CreateOrderRequest(
"test@example.com",
List.of(new OrderItemRequest(1L, 3))
);
ResponseEntity<OrderResponse> createResponse = restTemplate
.postForEntity("/api/v1/orders", request, OrderResponse.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
Long orderId = createResponse.getBody().id();
ResponseEntity<OrderResponse> getResponse = restTemplate
.getForEntity("/api/v1/orders/" + orderId, OrderResponse.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().customerEmail()).isEqualTo("test@example.com");
}
}
Docker i Kubernetes — wdrożenie produkcyjne#
Dockerfile z multi-stage build#
# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
Kubernetes Deployment#
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Tuning wydajności#
Wydajność aplikacji Spring Boot zależy od wielu czynników. Oto najważniejsze obszary optymalizacji.
Pule połączeń z HikariCP#
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
max-lifetime: 1200000
leak-detection-threshold: 60000
Cachowanie z Spring Cache#
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
return productRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
}
@CacheEvict(value = "products", key = "#id")
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
// aktualizacja produktu
}
@CacheEvict(value = "products", allEntries = true)
@Scheduled(fixedRate = 3600000)
public void evictAllProductsCache() {
// automatyczne czyszczenie cache co godzinę
}
}
Optymalizacja zapytań JPA#
// Problem N+1 — rozwiązanie z EntityGraph
@EntityGraph(attributePaths = {"items", "items.product"})
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findOrdersWithItems(@Param("email") String email);
// Projekcje — pobieranie tylko potrzebnych kolumn
public interface OrderSummaryProjection {
Long getId();
String getCustomerEmail();
OrderStatus getStatus();
BigDecimal getTotalAmount();
}
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<OrderSummaryProjection> findSummariesByStatus(@Param("status") OrderStatus status);
Kiedy wybrać Spring Boot?#
Spring Boot jest doskonałym wyborem, gdy:
- Budujesz aplikacje enterprise — dojrzały ekosystem z tysiącami bibliotek
- Potrzebujesz mikroserwisów — Spring Cloud zapewnia pełne wsparcie
- Twój zespół zna Javę — krzywa nauki Spring Boot jest łagodna
- Zależy Ci na wydajności — JVM oferuje znakomitą wydajność dla długotrwałych procesów
- Potrzebujesz bezpieczeństwa enterprise — Spring Security to standard branżowy
- Integracja z systemami legacy — Java ma najszerszą gamę konektorów
Rozważ alternatywy (Quarkus, Micronaut), jeśli priorytetem jest czas startu aplikacji lub zużycie pamięci w środowiskach serverless.
Podsumowanie#
Spring Boot to potężna platforma, która pokrywa praktycznie każdy aspekt budowy nowoczesnych aplikacji:
- Auto-konfiguracja radykalnie redukuje boilerplate
- Spring Data JPA upraszcza dostęp do danych
- Spring Security zapewnia bezpieczeństwo na poziomie enterprise
- Spring Cloud dostarcza infrastrukturę dla mikroserwisów
- Actuator umożliwia monitoring produkcyjny
- Testcontainers pozwalają na realistyczne testy integracyjne
Kluczem do sukcesu jest znajomość ekosystemu i stosowanie najlepszych praktyk od samego początku projektu.
Potrzebujesz wsparcia przy projektach Java?#
W MDS Software Solutions Group specjalizujemy się w budowie aplikacji enterprise z wykorzystaniem Spring Boot i mikroserwisów. Oferujemy:
- Projektowanie architektury mikroserwisowej
- Rozwój aplikacji Spring Boot od podstaw
- Migrację monolitów do mikroserwisów
- Wdrożenia w Docker i Kubernetes
- Audyt wydajności i bezpieczeństwa
- Wsparcie techniczne i szkolenia
Skontaktuj się z nami, aby omówić Twój projekt!
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.