🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Spring Boot 3 Guide 2026: REST APIs, JPA, Sicherheit und virtuelle Threads

⏱️5 min read  ·  1,038 words

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.

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.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা