Was ist Spring Boot?
Spring Boot ist eine Schicht über dem Spring Framework, die den Konfigurationsaufwand eliminiert, der früher in Spring erforderlich war. Anstatt XML-Dateien, Servlet-Konfigurationen und eines Containers für das Deployment zu benötigen, schreiben Sie eine Klasse mit einer main-Methode, fügen eine Starter-Dependency hinzu und erhalten eine laufende Anwendung mit einem eingebetteten Server.
Drei Kernkonzepte erledigen die Hauptarbeit. Starters bündeln die Abhängigkeiten für eine bestimmte Funktionalität in einer einzigen Zeile. Auto-configuration analysiert den Classpath und konfiguriert Beans auf Basis sinnvoller Standardwerte. Der embedded server bedeutet, dass die Anwendung ein normaler Java-Prozess ist – es muss nichts separat installiert oder bereitgestellt werden.
Das Ergebnis ist, dass Spring Boot zum Standardweg für die Entwicklung von JVM-Services geworden ist – von kleinen internen APIs bis hin zu großen Microservice-Flotten –, während die volle Leistungsfähigkeit des Spring-Ökosystems im Hintergrund erhalten bleibt.
Das Spring-Ökosystem in einer Minute
Spring ist nicht nur eine einzelne Library, sondern eine ganze Familie, die ein gemeinsames Programmiermodell nutzt:
- Spring Framework — der Core-Container, Dependency Injection und die Web-Layer.
- Spring Data — Repositories für SQL- und NoSQL-Speicher.
- Spring Security — Authentifizierung und Autorisierung.
- Spring Batch / Integration — Batch-Verarbeitung und Messaging.
- Spring Cloud — Konfiguration, Discovery und Resilience für verteilte Systeme.
Da diese Komponenten gemeinsam entwickelt wurden, kann ein Projekt sie nach und nach implementieren, ohne die Art und Weise der Programmierung ändern zu müssen. Diese Konsistenz ist der eigentliche Grund, warum Teams sich auf Spring standardisieren.
Starter und Dependency-Management
Ein Starter ist eine kuratierte Sammlung von Dependencies, die harmonisch zusammenarbeiten. Du deklarierst die gewünschte Funktionalität und nicht die einzelnen Jars.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Das Gradle-Äquivalent ist ebenso kurz:
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-validation"
runtimeOnly "org.postgresql:postgresql"
}
Das Parent-POM oder das dependency-management-Plugin legt die kompatiblen Versionen für dich fest. Überschreibe eine Version nur, wenn du einen triftigen Grund hast, und überlasse den Rest der Plattform.
Die Application-Klasse und der embedded Server
Jede Spring Boot Anwendung beginnt hier. Diese eine Klasse genügt, um einen Webserver zu starten.
package com.example.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
@SpringBootApplication kombiniert drei Annotationen: @Configuration, @EnableAutoConfiguration und @ComponentScan. Das Component Scanning beginnt im Package dieser Klasse und sucht in den darunterliegenden Packages. Aus diesem Grund sollte die Main-Klasse an der Wurzel Ihrer Package-Struktur liegen.
Das Ausführen von ./mvnw spring-boot:run startet einen embedded Tomcat auf Port 8080. Sie können Jetty oder Netty durch eine Änderung der Dependencies austauschen, ohne dass dies Auswirkungen auf den Anwendungscode hat.
Die annotierte Web-Layer
Spring MVC mappt HTTP-Requests mithilfe von Annotationen auf Methoden. @RestController kombiniert @Controller und @ResponseBody, sodass zurückgegebene Objekte von Jackson in JSON serialisiert werden.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<PostResponse> list(
@RequestParam(defaultValue = "0") int page) {
return service.findPage(page);
}
@GetMapping("/{id}")
public PostResponse get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
return service.create(request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
@PathVariable liest aus der URL, @RequestParam aus dem Query-String und @RequestBody deserialisiert den Payload. Halten Sie Controller schlank: Sie übersetzen HTTP in einen Aufruf eines Services und übersetzen das Ergebnis zurück. Die Domänenlogik gehört in die Service-Layer.
Dependency Injection und der Application Context
Der Application Context ist der Container, der Objekte (sogenannte Beans) erstellt und deren Abhängigkeiten bereitstellt. Sie legen fest, was eine Klasse benötigt; Spring entscheidet, wie diese erstellt wird.
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
public PostService(PostRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Transactional
public PostResponse create(CreatePostRequest request) {
var post = new Post(request.title(), request.body(), clock.instant());
return PostResponse.from(repository.save(post));
}
}
Stereotype-Annotationen markieren die zu verwaltenden Klassen: @Component für generische Beans, @Service für Domain-Services, @Repository für den Datenzugriff und @RestController für Web-Endpoints. Für alles, was Sie nicht annotieren können – etwa eine Drittanbieter-Klasse oder einen konfigurierten Client – deklarieren Sie dies in einer @Configuration Klasse.
@Configuration
class ClockConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
}
Bevorzugen Sie überall die Constructor Injection. Sie macht Abhängigkeiten explizit, ermöglicht final Felder und erlaubt es Ihnen, die Klasse in einem einfachen Unit-Test zu instanziieren, ohne Spring starten zu müssen.
Spring Data JPA und die Datenbank
Entities bilden Java-Objekte auf Tabellen ab, und Repositories ermöglichen den Datenzugriff ohne eine eigene Implementierung.
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Author author;
protected Post() {
}
public Post(String title, String body, Author author) {
this.title = title;
this.body = body;
this.author = author;
}
// getters and domain methods
}
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByPublishedTrueOrderByCreatedAtDesc();
@Query("select p from Post p join fetch p.author where p.id = :id")
Optional<Post> findWithAuthor(Long id);
}
Spring Data leitet eine Query aus dem Methodennamen ab, sodass findByPublishedTrueOrderByCreatedAtDesc keinen Body benötigt. Verwende @Query, wenn der abgeleitete Name unleserlich würde, und join fetch, um das Lazy-Loading N+1-Problem zu vermeiden. Setze ddl-auto in der Produktion auf validate und verwalte das Schema mit Flyway oder Liquibase.
Konfiguration mit application.yml und Profiles
Die Konfiguration befindet sich in src/main/resources und kann durch Umgebungsvariablen überschrieben werden – genau das ermöglicht es, dass dieselbe jar überall läuft.
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/blog
username: blog
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
server:
port: 8080
Profiles ermöglichen es, dass ein Build je nach Umgebung unterschiedlich reagiert.
# src/main/resources/application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
logging:
level:
root: warn
Aktiviere ein Profile mit --spring.profiles.active=prod oder der Umgebungsvariable SPRING_PROFILES_ACTIVE. @ConfigurationProperties bindet eine Gruppe von Einstellungen an eine typisierte Klasse, was sicherer ist, als @Value-Annotationen über die gesamte Codebasis zu verteilen.
Validierung
Jakarta Bean Validation Constraints an einem Request-Objekt werden erzwungen, wenn der Controller-Parameter mit @Valid annotiert ist.
public record CreatePostRequest(
@NotBlank @Size(max = 120) String title,
@NotBlank String body
) {
}
Ein @RestControllerAdvice zentralisiert die Fehlerantwort, sodass jeder Endpunkt das gleiche Format zurückgibt.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(first, second) -> first));
}
}
Validieren Sie an der Grenze (Boundary) und lassen Sie die Domain ihren Inputs vertrauen. In Kombination mit einem konsistenten Exception-Handler entfernt dies repetitive if (input == null)-Prüfungen aus jedem Service.
Spring Security im Überblick
Spring Security ist eine Filter-Chain vor Ihrer Anwendung. In modernem Spring Boot wird die Konfiguration über ein Bean anstelle von XML vorgenommen.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
}
Dieses einzelne Bean erfordert für jeden Endpunkt ein gültiges JWT, mit Ausnahme des Health-Checks. Regeln auf Methoden-Ebene mittels @PreAuthorize ermöglichen eine feingranulare Autorisierung dort, wo sie benötigt wird. Sicherheit ist ein komplexes Thema – planen Sie ein, die Dokumentation sorgfältig zu lesen, bevor Sie Funktionen im Internet veröffentlichen.
Actuator: Health und Metrics
Durch das Hinzufügen des Actuator-Starters werden betriebliche Endpunkte kostenlos bereitgestellt.
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
curl localhost:8080/actuator/health
/actuator/health lässt sich in Liveness- und Readiness-Probes integrieren, und /actuator/metrics liefert Statistiken zu JVM, HTTP und der Datenquelle. Stellen Sie nur das bereit, was Sie wirklich benötigen, und sichern Sie den Rest ab – diese Endpunkte geben viele Details über ein laufendes System preis.
Testing
@SpringBootTest startet den Applikationskontext und bildet die Basis für Integrationstests. MockMvc testet den Web-Layer, ohne einen echten Port zu öffnen.
@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listsPosts() throws Exception {
mockMvc.perform(get("/api/posts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Hello"));
}
}
Für ein schnelleres Feedback solltest du den Kontext auf den zu testenden Layer einschränken und die Collaborators mocken.
@WebMvcTest(PostController.class)
class PostControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private PostService service;
@Test
void returnsNotFound() throws Exception {
given(service.findById(1L)).willThrow(new PostNotFoundException(1L));
mockMvc.perform(get("/api/posts/1"))
.andExpect(status().isNotFound());
}
}
Nutze Slices für Controller und Repositories und behalte den vollständigen Kontext für einige wenige End-to-End-Tests vor. Testcontainers bietet dir eine echte Datenbank in den Tests, ohne dass die Persistenz gemockt werden muss.
Build und Deployment
Der Build-Prozess erzeugt ein ausführbares Fat Jar, das deine Klassen, Abhängigkeiten und den eingebetteten Server enthält.
./mvnw clean package
java -jar target/blog-0.0.1-SNAPSHOT.jar
Für Container erstellen Buildpacks ein optimiertes Image ohne Dockerfile.
./mvnw spring-boot:build-image
Die Konfiguration erfolgt über die Umgebungsvariablen, sodass dasselbe Artefakt unverändert von Staging nach Production überführt werden kann. Füge ein Dockerfile nur dann hinzu, wenn du eine Kontrolle benötigst, die Buildpacks nicht bieten, und bevorzuge einen Multi-Stage-Build, um das Image klein zu halten.
Best Practices
- Paketierung nach Features, sodass jede Domäne ihren eigenen Controller, Service und Repository besitzt.
- Nutze Constructor Injection und halte Felder
final. - Gib aus Controllern DTOs anstelle von JPA-Entities zurück.
- Validiere Request-Objekte mit
@Validund behandle Fehler in einem einzigen@RestControllerAdvice. - Halte
ddl-autoaufvalidateund verwalte Schema-Änderungen mit Flyway oder Liquibase. - Externalisiere die Konfiguration und nutze Profile, anstatt für jede Umgebung neu zu bauen.
- Füge Actuator hinzu, exponiere dann aber nur die Endpunkte und sichere diese, die du tatsächlich benötigst.
Häufige Fehler
- Field Injection mit
@Autowired, wodurch Abhängigkeiten versteckt werden und das Testen erschwert wird. - Direkte Serialisierung von Entities, was zu Leaks von Lazy-Loading oder internen Spalten führt.
- Business-Logik in den Controllern ansammeln, anstatt diese an Services zu delegieren.
- Verwendung von
ddl-auto: updatein der Produktion, was zu Abweichungen vom tatsächlichen Schema führt. - Starten des gesamten Contexts in jedem Unit-Test, was die Testsuite verlangsamt.
- Öffnen aller Actuator-Endpunkte für das öffentliche Internet.
- Hinzufügen von Startern, die nicht benötigt werden, was die Startup-Zeit verlängert.
Wie geht es weiter?
Spring Boot vermittelt Konzepte wie Dependency Injection, Auto-Configuration und Layered Design – Ideen, die weit über Java hinausgehen. Wenn Ihnen das Modul- und Injection-Modell gefällt, setzt NestJS dieses in TypeScript um, während Express die minimalistische Alternative aufzeigt. Die annotierte Web-Layer ist letztlich eine REST API, und da die meisten Anwendungen Daten in einem relationalen Store speichern, ist PostgreSQL der natürliche nächste Schritt.