Spring Boot ist das dominierende Java-Framework für Unternehmens-Backends im Jahr 2026. Mit Spring Boot 3.3, nativer Kompilierung über GraalVM, virtuellen Threads über Project Loom und dem Spring AI-Starter war die Erstellung von Java-APIs für die Produktion noch nie so schnell. Dieser Leitfaden führt Sie von „Hello World“ zur REST-API in Produktionsqualität.
📋 Table of Contents
Warum Spring Boot?
- Automatische Konfiguration– sinnvolle Standardeinstellungen, keine Boilerplate für gängige Muster
- Eingebettete Server— Tomcat/Netty enthalten, keine Bereitstellungs-WAR-Dateien
- Produktionsbereit— Aktuator, Mikrometermaße, integrierte Gesundheitsprüfungen
- GraalVM nativ— In native Binärdatei kompilieren, startet in <50 ms, verbraucht 50 % weniger RAM
- Riesiges Ökosystem– Spring Data, Sicherheit, Cloud, Batch, KI
Projekt-Setup
# Spring Initializr (start.spring.io)
# Or via curl:
curl https://start.spring.io/starter.zip -d dependencies=web,data-jpa,postgresql,security,actuator,validation,lombok -d type=maven-project -d language=java -d javaVersion=21 -d name=techpulse-api -d groupId=com.techpulse -d artifactId=api -o techpulse-api.zip
unzip techpulse-api.zip
cd techpulse-api
mvn spring-boot:run
application.yml-Konfiguration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/techpulse_db
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate # use Flyway/Liquibase for production
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
server:
port: 8080
compression:
enabled: true
management:
endpoints:
web:
exposure:
include: health,metrics,info,prometheus
endpoint:
health:
show-details: always
Entität und Repository
// User.java
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Size(min = 2, max = 50)
private String name;
@Column(unique = true, nullable = false)
@Email
private String email;
@Column(nullable = false)
@JsonIgnore
private String passwordHash;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role = Role.USER;
@Column(nullable = false)
private boolean active = true;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
public enum Role { USER, ADMIN, MODERATOR }
}
// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Page<User> findByActiveTrue(Pageable pageable);
List<User> findByRole(User.Role role);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.name ILIKE %:name%")
List<User> searchByName(@Param("name") String name);
}
Serviceschicht
// UserService.java
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {
private final UserRepository userRepo;
private final PasswordEncoder passwordEncoder;
@Transactional(readOnly = true)
public Page<UserDTO> listUsers(Pageable pageable) {
return userRepo.findByActiveTrue(pageable)
.map(UserDTO::from);
}
@Transactional(readOnly = true)
public UserDTO getUser(Long id) {
return userRepo.findById(id)
.map(UserDTO::from)
.orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));
}
public UserDTO createUser(CreateUserRequest request) {
if (userRepo.existsByEmail(request.email())) {
throw new ConflictException("Email already exists: " + request.email());
}
User user = User.builder()
.name(request.name())
.email(request.email())
.passwordHash(passwordEncoder.encode(request.password()))
.build();
User saved = userRepo.save(user);
log.info("Created user: {} ({})", saved.getEmail(), saved.getId());
return UserDTO.from(saved);
}
public UserDTO updateUser(Long id, UpdateUserRequest request) {
User user = userRepo.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
if (request.name() != null) user.setName(request.name());
return UserDTO.from(userRepo.save(user));
}
public void deleteUser(Long id) {
User user = userRepo.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
user.setActive(false); // soft delete
userRepo.save(user);
}
}
REST-Controller
// UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Validated
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> listUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
return ResponseEntity.ok(userService.listUsers(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUser(id));
}
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Valid @RequestBody CreateUserRequest request
) {
UserDTO created = userService.createUser(request);
URI location = URI.create("/api/v1/users/" + created.id());
return ResponseEntity.created(location).body(created);
}
@PatchMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request
) {
return ResponseEntity.ok(userService.updateUser(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
// DTOs (Java records)
public record UserDTO(Long id, String name, String email, User.Role role, LocalDateTime createdAt) {
public static UserDTO from(User user) {
return new UserDTO(user.getId(), user.getName(), user.getEmail(),
user.getRole(), user.getCreatedAt());
}
}
public record CreateUserRequest(
@NotBlank @Size(min=2, max=50) String name,
@NotBlank @Email String email,
@NotBlank @Size(min=8) String password
) {}
Globaler Ausnahmehandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Resource Not Found");
return problem;
}
@ExceptionHandler(ConflictException.class)
public ProblemDetail handleConflict(ConflictException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problem.setProperty("errors", errors);
return problem;
}
}
Frühlingssicherheit mit JWT
// JwtService.java
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
private static final long EXPIRY = 86400000L; // 24h
public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRY))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
Virtuelle Threads (Java 21 + Spring Boot 3.3)
# application.yml — enable virtual threads
spring:
threads:
virtual:
enabled: true # uses Project Loom virtual threads for Tomcat
Bei virtuellen Threads erhält jede HTTP-Anfrage einen einfachen virtuellen Thread anstelle eines Plattform-Threads. Dies verbessert den Durchsatz für I/O-gebundene Anwendungen (Datenbankabfragen, externe API-Aufrufe) erheblich, ohne dass Codeänderungen erforderlich sind.
Natives GraalVM-Image
# Add to pom.xml
# <plugin>spring-boot-maven-plugin with native goal</plugin>
# Build native image (requires GraalVM 22+)
mvn -Pnative native:compile
# Run (starts in ~50ms, uses ~60MB RAM vs 300MB JVM)
./target/techpulse-api
Spring Boot 3.3 im Jahr 2026 ist produktionsbereit, schnell und entwicklerfreundlich. Virtuelle Threads machen in den meisten Anwendungsfällen die Notwendigkeit einer reaktiven Programmierung überflüssig. Native GraalVM-Images machen Java zu einem ernsthaften Konkurrenten für Container-Microservices, bei denen Startzeit und Speicher eine Rolle spielen.
🔗 Share this article
✍️ Leave a Comment