⌂ PortalStart

Abschlussaudits

Vollständige Fach-, Architektur-, Pattern-, Security-, Dependency-, Test- und Release-Prüfungen über alle 34 Module.

Finaler Release-Audit34 Modul-AbschlussauditsFachlicher CodevergleichSecurity & DependenciesTestlückenArchitektur & Patterns
Semantisch neu geprüft

Legacy ↔ Modern

Desktop-Zentrale mit Suche, Filtern, echten Codepaaren und fachlichen Grenzen. Code wird nicht mehr nach Dateireihenfolge ausgewählt, sondern nach identischer fachlicher Rolle. Tests werden als Tests verglichen, Controller mit Endpoints, Publisher mit Sendern und Datenzugriff mit Datenzugriff.

Korrigiert: falsche Paarungen, generische Aussagen und die irreführende OSGi-Bezeichnung. Bei jedem Thema steht jetzt ausdrücklich, was gleich ist und was nicht gleichgesetzt werden darf.
Vollständige Modulabdeckung: 34 Module, 349 Quell-/Build-/Ressourcendateien und 104 semantische Vergleichsgruppen. Die zentralen Karten zeigen den fachlichen Kern; der Link „Vollständiger Modul-Codevergleich“ öffnet sämtliche Dateien des Moduls.
01Dependency InjectionService Locator und manuelle Registrierung → Constructor Injection mit SpringVergleich öffnen
Legacy

Service Locator und manuelle Registrierung

Der Aufrufer holt die Abhängigkeit zur Laufzeit aus einer globalen Registry.

Modern

Constructor Injection mit Spring

Der Container übergibt den fachlichen Port sichtbar über den Konstruktor.

Sauberer Vergleichsgegenstand: denselben Greeting-Service auflösen und ausführen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDer Aufrufer holt die Abhängigkeit zur Laufzeit aus einer globalen Registry.Der Container übergibt den fachlichen Port sichtbar über den Konstruktor.
KernunterschiedVersteckte Laufzeitauflösung wird durch eine explizite, unveränderliche Abhängigkeit ersetzt.
PatternsService LocatorPort, Constructor Injection
Nicht gleichsetzen: Service Locator und Dependency Injection sind gegensätzliche Verdrahtungsarten; ein Service Locator ist kein DI-Container.

Code mit gleicher fachlicher Rolle

Verdrahtung des gleichen Aufrufs

package at.aydin.lab.di.legacy;

public final class DiLegacyApplication {
    private DiLegacyApplication() {
    }

    public static void main(String[] args) {
        ServiceRegistry.register(GreetingService.class, new DefaultGreetingService());
        GreetingService service = ServiceRegistry.resolve(GreetingService.class);
        System.out.println(service.greet("Aydin"));
    }
}
package at.aydin.lab.di.modern;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class GreetingRunner implements CommandLineRunner {
    private final GreetingPort greetingPort;
    // Design Pattern: Constructor Injection
    // Zweck: Abhängigkeiten sind sichtbar, unveränderlich und leicht testbar.
    public GreetingRunner(GreetingPort greetingPort) {
        this.greetingPort = greetingPort;
    }

    @Override
    public void run(String ... args) {
        System.out.println(greetingPort.greet("Aydin"));
    }
}

Gleiche fachliche Implementierung

package at.aydin.lab.di.legacy;

public final class DefaultGreetingService implements GreetingService {
    @Override
    public String greet(String name) {
        return "Willkommen, " + name + "!";
    }
}
package at.aydin.lab.di.modern;

import org.springframework.stereotype.Service;

@Service
public class GermanGreetingService implements GreetingPort {
    @Override
    public String greet(String name) {
        return "Willkommen, " + name + "!";
    }
}

Empfohlene Migration

  1. Verhalten sichern: denselben Greeting-Service auflösen und ausführen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Service Locator und manuelle Registrierung hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Constructor Injection mit Spring implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
02AOPJDK Dynamic Proxy → Spring AOP mit @AspectVergleich öffnen
Legacy

JDK Dynamic Proxy

Ein InvocationHandler erzeugt den Proxy manuell und funktioniert nur über ein Interface.

Modern

Spring AOP mit @Aspect

Ein Spring-Aspect fängt die mit @Tracked markierte Methode am Container-Proxy ab.

Sauberer Vergleichsgegenstand: denselben Calculator-Aufruf messen, ohne die Addition mit Logging-Code zu vermischen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin InvocationHandler erzeugt den Proxy manuell und funktioniert nur über ein Interface.Ein Spring-Aspect fängt die mit @Tracked markierte Methode am Container-Proxy ab.
KernunterschiedProxy-Erzeugung und Join-Point-Auswahl wandern aus dem Anwendungscode in deklarative AOP-Konfiguration.
PatternsProxyAspect, Interceptor
Nicht gleichsetzen: Der direkte Unit-Test der Klasse prüft nur die Addition; das Aspect-Verhalten entsteht erst beim Aufruf über den Spring-Proxy.

Code mit gleicher fachlicher Rolle

Querschnittslogik

package at.aydin.lab.aop.legacy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

// Design Pattern: Proxy
// Zweck: Querschnittslogik wird vor und nach einem Methodenaufruf ausgeführt.
public final class LoggingProxy implements InvocationHandler {
    private final Object target;
    private LoggingProxy(Object target) {
        this.target = target;
    }

    public static <T> T wrap(Class<T> contract, T target) {
        Object proxy = Proxy.newProxyInstance(contract.getClassLoader(), new Class<?>[] {
            contract
        }, new LoggingProxy(target));
        return contract.cast(proxy);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long started = System.nanoTime();
        System.out.printf("START %s %s%n", method.getName(), Arrays.toString(args));
        try {
            return method.invoke(target, args);
        } finally {
            System.out.printf("ENDE %s %d µs%n", method.getName(), (System.nanoTime() - started) / 1_000);
        }
    }
}
package at.aydin.lab.aop.modern;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
// Design Pattern: Interceptor / Aspect
// Zweck: Monitoring bleibt getrennt von der Geschäftslogik.
public class TrackingAspect {
    @Around("@annotation(Tracked)")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long started = System.nanoTime();
        try {
            return joinPoint.proceed();
        } finally {
            long micros = (System.nanoTime() - started) / 1_000;
            System.out.printf("%s dauerte %d µs%n", joinPoint.getSignature().toShortString(), micros);
        }
    }
}

Gleicher Fachfall: Addition

package at.aydin.lab.aop.legacy;

public final class SimpleCalculator implements Calculator {
    @Override
    public int add(int left, int right) {
        return left + right;
    }
}
package at.aydin.lab.aop.modern;

import org.springframework.stereotype.Service;

@Service
public class TrackedCalculator implements Calculator {
    @Override
    @Tracked
    public int add(int left, int right) {
        return left + right;
    }
}

Empfohlene Migration

  1. Verhalten sichern: denselben Calculator-Aufruf messen, ohne die Addition mit Logging-Code zu vermischen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JDK Dynamic Proxy hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring AOP mit @Aspect implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
03RESTJAX-RS Resource mit javax.ws.rs → Spring MVC REST ControllerVergleich öffnen
Legacy

JAX-RS Resource mit javax.ws.rs

Die JAX-RS-Ressource greift direkt auf ein Singleton-Repository zu.

Modern

Spring MVC REST Controller

Der Controller delegiert über Constructor Injection an eine Service Layer.

Sauberer Vergleichsgegenstand: Books auflisten und neu anlegen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDie JAX-RS-Ressource greift direkt auf ein Singleton-Repository zu.Der Controller delegiert über Constructor Injection an eine Service Layer.
KernunterschiedHTTP-Adapter, Fachlogik und Datenzugriff werden in der modernen Variante klarer getrennt.
PatternsRepository, SingletonController, Service Layer, Repository
Nicht gleichsetzen: JAX-RS ist nicht grundsätzlich veraltet; die Legacy-Einordnung bezieht sich hier auf javax-Namespace, manuelle Verdrahtung und Beispielarchitektur.

Code mit gleicher fachlicher Rolle

HTTP-Adapter

package at.aydin.lab.rest.legacy;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;

@Path("books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
    private final BookRepository repository = BookRepository.instance();
    @GET
    public List<Book> all() {
        return repository.findAll();
    }

    @POST
    public Response create(Book book) {
        Book created = repository.save(book);
        return Response.created(URI.create("/api/books/" + created.getId())).entity(created).build();
    }
}
package at.aydin.lab.rest.modern;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {
    private final BookService service;
    public BookController(BookService service) {
        this.service = service;
    }

    @GetMapping
    public List<Book> all() {
        return service.findAll();
    }

    @PostMapping
    public ResponseEntity<Book> create(@Valid @RequestBody Book book, UriComponentsBuilder uriBuilder) {
        Book created = service.create(book);
        return ResponseEntity.created(uriBuilder.path("/api/books/{id}").build(created.id())).body(created);
    }
}

Datenzugriff

package at.aydin.lab.rest.legacy;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

// Design Pattern: Repository
// Zweck: Datenhaltung wird von der REST-Ressource getrennt.
public final class BookRepository {
    private static final BookRepository INSTANCE = new BookRepository();
    private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong sequence = new AtomicLong();
    private BookRepository() {
        save(new Book(0, "Enterprise Java verstehen"));
    }

    public static BookRepository instance() {
        return INSTANCE;
    }

    public List<Book> findAll() {
        return new ArrayList<>(books.values());
    }

    public Book save(Book book) {
        long id = book.getId() == 0 ? sequence.incrementAndGet() : book.getId();
        Book stored = new Book(id, book.getTitle());
        books.put(id, stored);
        return stored;
    }
}
package at.aydin.lab.rest.modern;

import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Repository
// Design Pattern: Repository
// Zweck: Der Controller kennt keine Details der Datenhaltung.
public class BookRepository {
    private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong sequence = new AtomicLong();
    public BookRepository() {
        save(new Book(0, "Modernes Enterprise Java"));
    }

    public List<Book> findAll() {
        return List.copyOf(books.values());
    }

    public Book save(Book input) {
        long id = input.id() == 0 ? sequence.incrementAndGet() : input.id();
        Book stored = new Book(id, input.title());
        books.put(id, stored);
        return stored;
    }
}

Empfohlene Migration

  1. Verhalten sichern: Books auflisten und neu anlegen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JAX-RS Resource mit javax.ws.rs hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring MVC REST Controller implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
04PersistenzDAO mit handgeschriebenem JDBC → Spring Data JPA RepositoryVergleich öffnen
Legacy

DAO mit handgeschriebenem JDBC

SQL, Connection-Handling, Schemaerzeugung und Mapping liegen in einer konkreten DAO-Klasse.

Modern

Spring Data JPA Repository

Spring Data erzeugt die Repository-Implementierung; eine Service Layer bildet die Transaktionsgrenze.

Sauberer Vergleichsgegenstand: Customers speichern und lesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusSQL, Connection-Handling, Schemaerzeugung und Mapping liegen in einer konkreten DAO-Klasse.Spring Data erzeugt die Repository-Implementierung; eine Service Layer bildet die Transaktionsgrenze.
KernunterschiedTechnischer Boilerplate-Code wird reduziert, während das Persistenzmodell explizit als Entity markiert wird.
PatternsDAORepository, Service Layer
Nicht gleichsetzen: DAO und Repository sind verwandte, aber nicht identische Patterns; Spring Data ersetzt nicht automatisch jede komplexe SQL-Optimierung.

Code mit gleicher fachlicher Rolle

Datenzugriffsvertrag

package at.aydin.lab.persistence.legacy;

import java.util.List;

// Design Pattern: DAO
// Zweck: SQL und JDBC werden hinter einer fachlichen Schnittstelle gekapselt.
public interface CustomerDao {
    Customer save(String name, String email);
    List<Customer> findAll();
}
package at.aydin.lab.persistence.modern;

import org.springframework.data.jpa.repository.JpaRepository;

// Design Pattern: Repository
// Zweck: Spring Data erzeugt die technische Implementierung aus dem Interface.
public interface CustomerRepository extends JpaRepository<CustomerEntity, Long> {
}

Persistenzmodell

package at.aydin.lab.persistence.legacy;

public record Customer(long id, String name, String email) {
}
package at.aydin.lab.persistence.modern;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customer")
public class CustomerEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    protected CustomerEntity() {
    }

    public CustomerEntity(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "CustomerEntity[id=" + id + ", name=" + name + ", email=" + email + "]";
    }
}

Empfohlene Migration

  1. Verhalten sichern: Customers speichern und lesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: DAO mit handgeschriebenem JDBC hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring Data JPA Repository implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
05TransaktionenManuelles JDBC Commit/Rollback → Deklaratives @TransactionalVergleich öffnen
Legacy

Manuelles JDBC Commit/Rollback

Die Service-Methode öffnet die Connection und steuert Commit, Rollback und Auto-Commit selbst.

Modern

Deklaratives @Transactional

Die Service-Methode beschreibt die Transaktionsgrenze deklarativ; das Repository übernimmt den Datenzugriff.

Sauberer Vergleichsgegenstand: Geld zwischen zwei Konten übertragen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDie Service-Methode öffnet die Connection und steuert Commit, Rollback und Auto-Commit selbst.Die Service-Methode beschreibt die Transaktionsgrenze deklarativ; das Repository übernimmt den Datenzugriff.
KernunterschiedTransaktionsinfrastruktur wird aus dem Fachablauf entfernt.
PatternsTransaction ScriptService Layer, Repository, Declarative Transaction
Nicht gleichsetzen: @Transactional wirkt nur bei Aufrufen über den Spring-Proxy; Self-Invocation ist eine bekannte Grenze.

Code mit gleicher fachlicher Rolle

Gleicher Transfer-Fachfall

package at.aydin.lab.transactions.legacy;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

// Design Pattern: Transaction Script
// Zweck: Ein kompletter fachlicher Ablauf wird explizit in einer Methode transaktional ausgeführt.
public final class LegacyTransferService {
    private final String url;
    public LegacyTransferService(String url) {
        this.url = url;
        initialize();
    }

    private Connection connection() throws SQLException {
        return DriverManager.getConnection(url, "sa", "");
    }

    private void initialize() {
        try (Connection c = connection(); Statement s = c.createStatement()) {
            s.executeUpdate("create table if not exists account(id bigint primary key, balance decimal(15,2))");
            s.executeUpdate("merge into account key(id) values (1,1000.00),(2,200.00)");
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    public void transfer(long from, long to, BigDecimal amount, boolean simulateFailure) {
        try (Connection c = connection()) {
            c.setAutoCommit(false);
            try {
                update(c, from, amount.negate());
                if (simulateFailure) throw new IllegalStateException("Simulierter Fehler");
                update(c, to, amount);
                c.commit();
            } catch (Exception e) {
                c.rollback();
                throw new IllegalStateException("Überweisung zurückgerollt", e);
            }
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    private void update(Connection c, long id, BigDecimal delta) throws SQLException {
        try (PreparedStatement ps = c.prepareStatement("update account set balance=balance+? where id=?")) {
            ps.setBigDecimal(1, delta);
            ps.setLong(2, id);
            if (ps.executeUpdate() != 1) throw new SQLException("Konto fehlt: " + id);
        }
    }

    public BigDecimal balance(long id) {
        try (Connection c = connection(); PreparedStatement ps = c.prepareStatement("select balance from account where id=?")) {
            ps.setLong(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) throw new IllegalArgumentException();
                return rs.getBigDecimal(1);
            }
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }
}
package at.aydin.lab.transactions.modern;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
// Design Pattern: Service Layer + declarative Transaction Boundary
// Zweck: Rollback-Regeln werden vom Framework statt durch manuelles JDBC gesteuert.
public class TransferService {
    private final AccountRepository repository;
    public TransferService(AccountRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void transfer(long from, long to, BigDecimal amount, boolean simulateFailure) {
        repository.changeBalance(from, amount.negate());
        if (simulateFailure) throw new IllegalStateException("Simulierter Fehler");
        repository.changeBalance(to, amount);
    }
}

Empfohlene Migration

  1. Verhalten sichern: Geld zwischen zwei Konten übertragen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Manuelles JDBC Commit/Rollback hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Deklaratives @Transactional implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
06BatchManueller Dateiimport → Spring-Batch Job mit Reader/Processor/WriterVergleich öffnen
Legacy

Manueller Dateiimport

Ein eigener Importer liest Zeilen, überspringt den Header und mappt sie in einem Durchlauf.

Modern

Spring-Batch Job mit Reader/Processor/Writer

Reader, Processor, Writer, Step und Job sind getrennte, wiederanlaufbare Batch-Bausteine.

Sauberer Vergleichsgegenstand: CSV-Bestellungen einlesen, prüfen und persistieren.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin eigener Importer liest Zeilen, überspringt den Header und mappt sie in einem Durchlauf.Reader, Processor, Writer, Step und Job sind getrennte, wiederanlaufbare Batch-Bausteine.
KernunterschiedEin selbstgebauter Ablauf wird zu einer expliziten Pipeline mit Job-Metadaten und Transaktionsgrenzen.
PatternsMapper, manuelles Template MethodPipeline, Item Reader/Processor/Writer
Nicht gleichsetzen: Für sehr kleine Einmalimporte kann Spring Batch mehr Infrastruktur als nötig sein.

Code mit gleicher fachlicher Rolle

Gesamter Importablauf

package at.aydin.lab.batch.legacy;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

// Design Pattern: Template Method (manuell angedeutet)
// Zweck: Lesen, Überspringen, Parsen und Schreiben bilden einen festen Batch-Ablauf.
public final class LegacyBatchImporter {
    private final CsvOrderParser parser = new CsvOrderParser();
    public List<OrderRow> importFile(Path path) throws IOException {
        try (var lines = Files.lines(path)) {
            return lines.skip(1).filter(line -> !line.isBlank()).map(parser::parse).toList();
        }
    }
}
package at.aydin.lab.batch.modern;

import javax.sql.DataSource;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
// Design Pattern: Pipeline
// Zweck: Reader, Processor und Writer sind austauschbare, getrennt testbare Batch-Stufen.
public class BatchConfiguration {
    @Bean
    FlatFileItemReader<OrderRow> orderReader() {
        return new FlatFileItemReaderBuilder<OrderRow>()
                .name("orderReader")
                .resource(new ClassPathResource("sample-orders.csv"))
                .linesToSkip(1)
                .delimited()
                .names("id", "customer", "amount")
                .fieldSetMapper(fieldSet -> new OrderRow(
                        fieldSet.readLong("id"),
                        fieldSet.readString("customer"),
                        fieldSet.readBigDecimal("amount")))
                .build();
    }

    @Bean
    ItemProcessor<OrderRow, OrderRow> positiveAmountProcessor() {
        return item -> item.amount().signum() > 0 ? item : null;
    }

    @Bean
    JdbcBatchItemWriter<OrderRow> orderWriter(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<OrderRow>()
                .dataSource(dataSource)
                .sql("insert into imported_order(id,customer,amount) "
                        + "values (:id,:customer,:amount)")
                .itemSqlParameterSourceProvider(item -> new MapSqlParameterSource()
                        .addValue("id", item.id())
                        .addValue("customer", item.customer())
                        .addValue("amount", item.amount()))
                .build();
    }

    @Bean
    Step importOrders(
            JobRepository jobRepository,
            PlatformTransactionManager transactionManager,
            FlatFileItemReader<OrderRow> orderReader,
            ItemProcessor<OrderRow, OrderRow> positiveAmountProcessor,
            JdbcBatchItemWriter<OrderRow> orderWriter) {
        return new StepBuilder("importOrders", jobRepository)
                .<OrderRow, OrderRow>chunk(10, transactionManager)
                .reader(orderReader)
                .processor(positiveAmountProcessor)
                .writer(orderWriter)
                .build();
    }

    @Bean
    Job importOrderJob(JobRepository jobRepository, Step importOrders) {
        return new JobBuilder("importOrderJob", jobRepository)
                .start(importOrders)
                .build();
    }
}

Empfohlene Migration

  1. Verhalten sichern: CSV-Bestellungen einlesen, prüfen und persistieren mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Manueller Dateiimport hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring-Batch Job mit Reader/Processor/Writer implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
07MessagingJMS Gateway mit javax.jms → Spring JmsTemplate PublisherVergleich öffnen
Legacy

JMS Gateway mit javax.jms

Connection, Session, Producer und Message werden im Gateway manuell verwaltet.

Modern

Spring JmsTemplate Publisher

JmsTemplate kapselt Ressourcenverwaltung und Message-Konvertierung.

Sauberer Vergleichsgegenstand: eine Order-Nachricht an eine Queue senden.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusConnection, Session, Producer und Message werden im Gateway manuell verwaltet.JmsTemplate kapselt Ressourcenverwaltung und Message-Konvertierung.
KernunterschiedNiedrige JMS-Ressourcenverwaltung wird durch ein Framework-Gateway ersetzt.
PatternsGatewayGateway, Template
Nicht gleichsetzen: Der Demo-Runner ist kein Gegenstück zum Legacy-Gateway; korrekt verglichen werden Gateway und Publisher.

Code mit gleicher fachlicher Rolle

Nachrichtenversand

package at.aydin.lab.messaging.legacy;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;

// Design Pattern: Messaging Gateway
// Zweck: JMS-Infrastruktur wird hinter einfachen Sende-/Empfangsmethoden gekapselt.
public final class LegacyJmsGateway {
    private final ConnectionFactory connectionFactory;
    public LegacyJmsGateway(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    public void send(String queueName, String payload) throws Exception {
        try (Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false,
            Session.AUTO_ACKNOWLEDGE)) {
            Queue queue = session.createQueue(queueName);
            try (MessageProducer producer = session.createProducer(queue)) {
                producer.send(session.createTextMessage(payload));
            }
        }
    }

    public String receive(String queueName, long timeoutMillis) throws Exception {
        try (Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false,
            Session.AUTO_ACKNOWLEDGE)) {
            connection.start();
            Queue queue = session.createQueue(queueName);
            try (MessageConsumer consumer = session.createConsumer(queue)) {
                return ((TextMessage) consumer.receive(timeoutMillis)).getText();
            }
        }
    }
}
package at.aydin.lab.messaging.modern;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;

@Component
// Design Pattern: Messaging Gateway
// Zweck: Fachcode muss weder Connection noch Session verwalten.
public class OrderMessagePublisher {
    private final JmsTemplate jmsTemplate;
    public OrderMessagePublisher(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void publish(String orderNumber) {
        jmsTemplate.convertAndSend("orders", orderNumber);
    }
}

Empfohlene Migration

  1. Verhalten sichern: eine Order-Nachricht an eine Queue senden mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JMS Gateway mit javax.jms hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring JmsTemplate Publisher implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
08XML und JAXBJAXB javax.xml.bind → Jakarta XML BindingVergleich öffnen
Legacy

JAXB javax.xml.bind

Das Mapping verwendet den früheren javax.xml.bind-Namespace.

Modern

Jakarta XML Binding

Die gleiche Mapper-Grenze verwendet Jakarta XML Binding und moderne Runtime-Abhängigkeiten.

Sauberer Vergleichsgegenstand: Customer-Objekte in XML schreiben und zurücklesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDas Mapping verwendet den früheren javax.xml.bind-Namespace.Die gleiche Mapper-Grenze verwendet Jakarta XML Binding und moderne Runtime-Abhängigkeiten.
KernunterschiedFachliches Verhalten bleibt gleich; API-Namespace und Laufzeitabhängigkeiten ändern sich.
PatternsMapperMapper
Nicht gleichsetzen: Ein Namespace-Wechsel allein ist keine Architekturmodernisierung; XML-Schema-Kompatibilität muss separat getestet werden.

Code mit gleicher fachlicher Rolle

Gleicher XML-Mapper

package at.aydin.lab.xml.legacy;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import java.io.StringWriter;

// Design Pattern: Mapper
// Zweck: Domänenobjekt und XML-Repräsentation werden kontrolliert ineinander überführt.
public final class LegacyXmlMapper {
    public String toXml(LegacyCustomer customer) throws Exception {
        JAXBContext context = JAXBContext.newInstance(LegacyCustomer.class);
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        StringWriter writer = new StringWriter();
        marshaller.marshal(customer, writer);
        return writer.toString();
    }
}
package at.aydin.lab.xml.modern;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
import java.io.StringWriter;

// Design Pattern: Mapper
// Zweck: Jakarta JAXB kapselt die XML-Serialisierung hinter einer klaren API.
public final class ModernXmlMapper {
    public String toXml(ModernCustomer customer) throws Exception {
        JAXBContext context = JAXBContext.newInstance(ModernCustomer.class);
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        StringWriter writer = new StringWriter();
        marshaller.marshal(customer, writer);
        return writer.toString();
    }
}

Empfohlene Migration

  1. Verhalten sichern: Customer-Objekte in XML schreiben und zurücklesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JAXB javax.xml.bind hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Jakarta XML Binding implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
09Servlet und WebHttpServlet → Spring REST ControllerVergleich öffnen
Legacy

HttpServlet

Das Servlet schreibt Status, Content-Type und Response-Body direkt über HttpServletResponse.

Modern

Spring REST Controller

Der Controller gibt ein typisiertes Response-Objekt zurück; Serialisierung übernimmt Spring MVC.

Sauberer Vergleichsgegenstand: einen Status-Endpunkt bereitstellen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDas Servlet schreibt Status, Content-Type und Response-Body direkt über HttpServletResponse.Der Controller gibt ein typisiertes Response-Objekt zurück; Serialisierung übernimmt Spring MVC.
KernunterschiedServlet-Infrastruktur wird durch einen deklarativen HTTP-Adapter ersetzt.
PatternsFront Controller / Servlet AdapterController, DTO
Nicht gleichsetzen: Spring MVC läuft weiterhin auf Servlet-Infrastruktur; es abstrahiert sie, ersetzt sie nicht vollständig.

Code mit gleicher fachlicher Rolle

Status-Endpunkt

package at.aydin.lab.web.legacy;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setStatus(200);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(statusJson());
    }

    // Design Pattern: Front Controller (kleine Demonstration)
    // Zweck: Ein zentraler Servlet-Endpunkt verarbeitet den HTTP-Aufruf.
    static String statusJson() {
        return "{\"status\":\"UP\",\"stack\":\"javax.servlet\"}";
    }
}
package at.aydin.lab.web.modern;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
// Design Pattern: Front Controller + MVC Controller
// Zweck: Spring MVC übernimmt Routing, Konvertierung und HTTP-Infrastruktur.
public class StatusController {
    @GetMapping("/status")
    public Map<String, String> status() {
        return Map.of("status", "UP", "stack", "Spring MVC / jakarta.servlet");
    }
}

Empfohlene Migration

  1. Verhalten sichern: einen Status-Endpunkt bereitstellen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: HttpServlet hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring REST Controller implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
10SOAPJAX-WS mit javax.jws → Jakarta XML Web ServicesVergleich öffnen
Legacy

JAX-WS mit javax.jws

Endpoint und Annotationen verwenden den javax-Namespace.

Modern

Jakarta XML Web Services

Endpoint und Annotationen verwenden die Jakarta-API.

Sauberer Vergleichsgegenstand: eine Calculator-SOAP-Operation veröffentlichen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEndpoint und Annotationen verwenden den javax-Namespace.Endpoint und Annotationen verwenden die Jakarta-API.
KernunterschiedDer Fachvertrag bleibt gleich; API-Namespace und Runtime werden modernisiert.
PatternsService EndpointService Endpoint
Nicht gleichsetzen: SOAP-Vertragskompatibilität richtet sich nach WSDL und XML-Schema, nicht nur nach Java-Packages.

Code mit gleicher fachlicher Rolle

Gleicher SOAP-Endpunkt

package at.aydin.lab.soap.legacy;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService(serviceName = "LegacyCalculatorService")
// Design Pattern: Remote Facade
// Zweck: Eine grobe, stabile SOAP-Schnittstelle schützt die interne Fachlogik.
public class LegacyCalculatorEndpoint {
    @WebMethod
    public int add(int left, int right) {
        return left + right;
    }
}
package at.aydin.lab.soap.modern;

import jakarta.jws.WebMethod;
import jakarta.jws.WebService;

@WebService(serviceName = "ModernCalculatorService")
// Design Pattern: Remote Facade
// Zweck: Der Vertrag bleibt stabil, während die Laufzeit auf Jakarta migriert wird.
public class ModernCalculatorEndpoint {
    @WebMethod
    public int add(int left, int right) {
        return left + right;
    }
}

Empfohlene Migration

  1. Verhalten sichern: eine Calculator-SOAP-Operation veröffentlichen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JAX-WS mit javax.jws hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Jakarta XML Web Services implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
11Java-GrundlagenMutable JavaBean-Klasse → Record und EnumVergleich öffnen
Legacy

Mutable JavaBean-Klasse

Felder, Konstruktor, Getter, Setter, equals/hashCode und toString werden manuell gepflegt.

Modern

Record und Enum

Ein Record bildet unveränderliche Daten kompakt ab; der Status ist ein Enum statt freier Text.

Sauberer Vergleichsgegenstand: denselben Customer-Datensatz modellieren.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusFelder, Konstruktor, Getter, Setter, equals/hashCode und toString werden manuell gepflegt.Ein Record bildet unveränderliche Daten kompakt ab; der Status ist ein Enum statt freier Text.
KernunterschiedMutabilität und Boilerplate werden reduziert, während erlaubte Zustände typisiert werden.
PatternsJavaBeanRecord, Value Object
Nicht gleichsetzen: Records sind nicht für jedes Entity- oder Framework-Modell geeignet; Identität und Mutabilität müssen bewusst entschieden werden.

Code mit gleicher fachlicher Rolle

Gleiches Customer-Modell

package at.aydin.lab.basics.legacy;

// Design Pattern: JavaBean
// Zweck: Historische mutable Datenstruktur mit leerem Konstruktor und Settern.
public class LegacyCustomer {
    private long id;
    private String name;
    private String status;
    public LegacyCustomer() {
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String displayText() {
        return id + ": " + name + " [" + status + "]";
    }
}
package at.aydin.lab.basics.modern;

// Design Pattern: Value Object
// Zweck: Unveränderliche, validierte fachliche Daten mit wertbasierter Gleichheit.
public record Customer(long id, String name, CustomerStatus status) {
    public Customer {
        if (id <= 0) throw new IllegalArgumentException("id muss positiv sein");
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name fehlt");
        if (status == null) throw new IllegalArgumentException("status fehlt");
    }

    public String displayText() {
        return "%d: %s [%s]".formatted(id, name, status);
    }
}

Empfohlene Migration

  1. Verhalten sichern: denselben Customer-Datensatz modellieren mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Mutable JavaBean-Klasse hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Record und Enum implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
12CollectionsMutable Listen und lineare Suche → Map, immutable Rückgaben und moderne Collection-APIsVergleich öffnen
Legacy

Mutable Listen und lineare Suche

Die Implementierung arbeitet mit einer Liste und gibt intern veränderbare Strukturen leichter preis.

Modern

Map, immutable Rückgaben und moderne Collection-APIs

Eine Map bildet den Schlüsselzugriff direkt ab und Rückgaben werden kontrolliert kopiert.

Sauberer Vergleichsgegenstand: Inventory-Einträge speichern und suchen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDie Implementierung arbeitet mit einer Liste und gibt intern veränderbare Strukturen leichter preis.Eine Map bildet den Schlüsselzugriff direkt ab und Rückgaben werden kontrolliert kopiert.
KernunterschiedDatenstruktur und API werden an Zugriffsmuster und Immutability ausgerichtet.
PatternsCollection WrapperCollection Wrapper, Defensive Copy
Nicht gleichsetzen: Map ist nicht pauschal besser als List; die richtige Collection hängt von Ordnung, Duplikaten und Zugriffsmuster ab.

Code mit gleicher fachlicher Rolle

Gleiches Inventory

package at.aydin.lab.collections.legacy;

import java.util.*;

// Design Pattern: Collection Wrapper
// Zweck: Kapselt alte Vector-/Hashtable-Strukturen hinter fachlichen Methoden.
public final class LegacyInventory {
    private final Vector<String> order = new Vector<>();
    private final Hashtable<String, Integer> stock = new Hashtable<>();
    public void add(String sku, int quantity) {
        if (!stock.containsKey(sku)) order.add(sku);
        stock.put(sku, stock.getOrDefault(sku, 0) + quantity);
    }

    public int quantity(String sku) {
        return stock.getOrDefault(sku, 0);
    }

    public Vector<String> orderedSkus() {
        return new Vector<>(order);
    }
}
package at.aydin.lab.collections.modern;

import java.util.*;

// Design Pattern: Snapshot
// Zweck: Gibt unveränderliche Momentaufnahmen statt interner mutabler Collections zurück.
public final class Inventory {
    private final LinkedHashMap<String, Integer> stock = new LinkedHashMap<>();
    public void add(String sku, int quantity) {
        Objects.requireNonNull(sku);
        if (quantity <= 0) throw new IllegalArgumentException("quantity");
        stock.merge(sku, quantity, Integer::sum);
    }

    public int quantity(String sku) {
        return stock.getOrDefault(sku, 0);
    }

    public Map<String, Integer> snapshot() {
        return Collections.unmodifiableMap(new LinkedHashMap<>(stock));
    }

    public List<String> orderedSkus() {
        return List.copyOf(stock.sequencedKeySet());
    }
}

Empfohlene Migration

  1. Verhalten sichern: Inventory-Einträge speichern und suchen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Mutable Listen und lineare Suche hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Map, immutable Rückgaben und moderne Collection-APIs implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
13GenericsRaw Type Box → Typisierte Generic BoxVergleich öffnen
Legacy

Raw Type Box

RawBox speichert Object; falsche Casts scheitern erst zur Laufzeit.

Modern

Typisierte Generic Box

TypedBox<T> legt den Typ beim Kompilieren fest.

Sauberer Vergleichsgegenstand: einen Wert in einer Box speichern und ohne unsicheren Cast lesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusRawBox speichert Object; falsche Casts scheitern erst zur Laufzeit.TypedBox<T> legt den Typ beim Kompilieren fest.
KernunterschiedLaufzeit-ClassCastException wird durch Compile-Time-Typsicherheit verhindert.
PatternsRaw ContainerGeneric Container
Nicht gleichsetzen: Type Erasure bleibt bestehen; nicht jede Laufzeit-Typinformation ist über T verfügbar.

Code mit gleicher fachlicher Rolle

Gleiche Box-Funktion

package at.aydin.lab.generics.legacy;

// Design Pattern: Generic Container (Legacy) – kapselt Werte, verliert durch Raw Types jedoch Typsicherheit.
@SuppressWarnings({
    "rawtypes", "unchecked"
}) public final class RawBox {
    private Object value;
    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}
package at.aydin.lab.generics.modern;

// Design Pattern: Generic Container
// Zweck: Der gespeicherte Typ wird zur Compile-Zeit festgelegt; Laufzeit-Casts entfallen.
public final class TypedBox<T> {
    private T value;
    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

Test des gleichen Verhaltens

package at.aydin.lab.generics.legacy;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class RawBoxTest {
    @Test
    void requiresRuntimeCast() {
        var b = new RawBox();
        b.set("text");
        assertEquals("text", (String) b.get());
        assertThrows(ClassCastException.class, () -> {
            Integer ignored = (Integer) b.get();
        });
    }
}
package at.aydin.lab.generics.modern;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class TypedBoxTest {
    @Test
    void preservesTheDeclaredType() {
        TypedBox<String> box = new TypedBox<>();
        box.set("text");
        assertEquals("text", box.get());
    }
}

Empfohlene Migration

  1. Verhalten sichern: einen Wert in einer Box speichern und ohne unsicheren Cast lesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Raw Type Box hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Typisierte Generic Box implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
14Annotations und ReflectionNamenskonvention und Reflection → Eigene @Command-AnnotationVergleich öffnen
Legacy

Namenskonvention und Reflection

Methoden werden über Namen oder implizite Konventionen erkannt.

Modern

Eigene @Command-Annotation

Eine Runtime-Annotation markiert die erlaubten Methoden explizit.

Sauberer Vergleichsgegenstand: Command-Methoden entdecken und ausführen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusMethoden werden über Namen oder implizite Konventionen erkannt.Eine Runtime-Annotation markiert die erlaubten Methoden explizit.
KernunterschiedImplizite Namenslogik wird zu maschinenlesbaren Metadaten.
PatternsRegistry, Convention over ConfigurationAnnotation-driven Registry
Nicht gleichsetzen: Reflection bleibt auch in der modernen Variante vorhanden; verbessert wird die explizite Auswahl, nicht die grundsätzliche Laufzeitmechanik.

Code mit gleicher fachlicher Rolle

Command Registry

package at.aydin.lab.annotations.legacy;

import java.lang.reflect.*;

// Design Pattern: Registry
// Zweck: Historische Registrierung über String-Konventionen und Reflection.
public final class LegacyCommandRegistry {
    private final Object target;
    public LegacyCommandRegistry(Object target) {
        this.target = target;
    }

    public String execute(String command, String argument) {
        try {
            Method m = target.getClass().getMethod("command_" + command, String.class);
            return (String) m.invoke(target, argument);
        } catch (ReflectiveOperationException ex) {
            throw new IllegalArgumentException("Unbekanntes Kommando: " + command, ex);
        }
    }
}
package at.aydin.lab.annotations.modern;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

// Design Pattern: Command + Metadata Registry
// Zweck: Annotationen machen Namen explizit und entkoppeln sie von Methodenkonventionen.
public final class CommandRegistry {
    private record Handler(Object target, Method method) {}

    private final Map<String, Handler> handlers = new HashMap<>();

    public CommandRegistry(Object... targets) {
        for (Object target : targets) {
            for (Method method : target.getClass().getDeclaredMethods()) {
                Command command = method.getAnnotation(Command.class);
                if (command != null) {
                    handlers.put(command.value(), new Handler(target, method));
                }
            }
        }
    }

    public String execute(String command, String argument) {
        Handler handler = Optional.ofNullable(handlers.get(command))
                .orElseThrow(() -> new IllegalArgumentException(
                        "Unbekanntes Kommando: " + command));
        try {
            return (String) handler.method().invoke(handler.target(), argument);
        } catch (ReflectiveOperationException exception) {
            throw new IllegalStateException(exception);
        }
    }
}

Command-Definitionen

package at.aydin.lab.annotations.legacy;

public final class LegacyCommands {
    public String command_hello(String name) {
        return "Hallo " + name;
    }

    public String command_bye(String name) {
        return "Tschüss " + name;
    }
}
package at.aydin.lab.annotations.modern;

public final class ModernCommands {
    @Command("hello")
    public String greet(String name) {
        return "Hallo " + name;
    }

    @Command("bye")
    public String farewell(String name) {
        return "Tschüss " + name;
    }
}

Empfohlene Migration

  1. Verhalten sichern: Command-Methoden entdecken und ausführen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Namenskonvention und Reflection hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Eigene @Command-Annotation implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
15Lambda und StreamsSchleifen und mutable Akkumulatoren → Streams und CollectorsVergleich öffnen
Legacy

Schleifen und mutable Akkumulatoren

Mehrere Schleifen verändern Zwischenergebnisse manuell.

Modern

Streams und Collectors

Filter, Mapping und Aggregation werden als deklarative Pipeline beschrieben.

Sauberer Vergleichsgegenstand: Order-Daten filtern und aggregieren.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusMehrere Schleifen verändern Zwischenergebnisse manuell.Filter, Mapping und Aggregation werden als deklarative Pipeline beschrieben.
KernunterschiedDer Code beschreibt stärker das gewünschte Ergebnis statt den Kontrollfluss.
PatternsImperative PipelineFunctional Pipeline
Nicht gleichsetzen: Streams sind nicht automatisch schneller; Lesbarkeit, Boxing und Parallelisierung müssen geprüft werden.

Code mit gleicher fachlicher Rolle

Gleiche Order-Analyse

package at.aydin.lab.streams.legacy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// Design Pattern: Accumulator
// Zweck: Sammelt Ergebnisse imperativ in einer veränderlichen Collection.
public final class LegacyOrderAnalytics {
    public List<String> paidCustomersOver(List<Order> orders, double limit) {
        List<String> result = new ArrayList<>();
        for (Order order : orders) {
            if (order.paid()
                    && order.total() > limit
                    && !result.contains(order.customer())) {
                result.add(order.customer());
            }
        }
        Collections.sort(result);
        return result;
    }
}
package at.aydin.lab.streams.modern;

import java.util.*;

// Design Pattern: Pipeline
// Zweck: Filter-, Mapping- und Reduktionsschritte werden deklarativ zusammengesetzt.
public final class OrderAnalytics {
    public List<String> paidCustomersOver(List<Order> orders, double limit) {
        return orders.stream().filter(Order::paid).filter(o -> o.total() > limit).map(Order::customer).distinct()
                .sorted().toList();
    }

    public double totalRevenue(List<Order> orders) {
        return orders.stream().filter(Order::paid).mapToDouble(Order::total).sum();
    }
}

Empfohlene Migration

  1. Verhalten sichern: Order-Daten filtern und aggregieren mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Schleifen und mutable Akkumulatoren hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Streams und Collectors implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
16ConcurrencyFester Thread Pool → Virtual ThreadsVergleich öffnen
Legacy

Fester Thread Pool

Ein begrenzter Platform-Thread-Pool muss dimensioniert und beendet werden.

Modern

Virtual Threads

Pro Task wird ein leichtgewichtiger Virtual Thread verwendet.

Sauberer Vergleichsgegenstand: viele unabhängige blockierende Tasks ausführen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin begrenzter Platform-Thread-Pool muss dimensioniert und beendet werden.Pro Task wird ein leichtgewichtiger Virtual Thread verwendet.
KernunterschiedThread-Knappheit für blockierende I/O-Workloads wird deutlich reduziert.
PatternsExecutorThread-per-Task Executor
Nicht gleichsetzen: Virtual Threads beschleunigen CPU-bound Arbeit nicht und ersetzen keine Synchronisations- oder Backpressure-Konzepte.

Code mit gleicher fachlicher Rolle

Gleicher Task Runner

package at.aydin.lab.concurrency.legacy;

import java.util.*;
import java.util.concurrent.*;

// Design Pattern: Task Executor
// Zweck: Begrenzter Plattform-Thread-Pool führt blockierende Aufgaben aus.
public final class LegacyTaskRunner {
    public List<String> run(List<Callable<String>> tasks) {
        ExecutorService pool = Executors.newFixedThreadPool(4);
        try {
            List<String> result = new ArrayList<>();
            for (Future<String> f : pool.invokeAll(tasks)) result.add(f.get());
            return result;
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(ex);
        } catch (ExecutionException ex) {
            throw new IllegalStateException(ex.getCause());
        } finally {
            pool.shutdown();
        }
    }
}
package at.aydin.lab.concurrency.modern;

import java.util.*;
import java.util.concurrent.*;

// Design Pattern: Task Executor
// Zweck: Ein Virtual Thread pro blockierender Aufgabe vereinfacht skalierbare Nebenläufigkeit.
public final class VirtualThreadTaskRunner {
    public List<String> run(List<Callable<String>> tasks) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = tasks.stream().map(executor::submit).toList();
            List<String> result = new ArrayList<>(futures.size());
            for (Future<String> future : futures) result.add(future.get());
            return List.copyOf(result);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(ex);
        } catch (ExecutionException ex) {
            throw new IllegalStateException(ex.getCause());
        }
    }
}

Empfohlene Migration

  1. Verhalten sichern: viele unabhängige blockierende Tasks ausführen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Fester Thread Pool hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Virtual Threads implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
17Java I/O und NIOFile und manuelle Streams → Path und FilesVergleich öffnen
Legacy

File und manuelle Streams

Streams, Buffer und Close-Logik werden manuell verwaltet.

Modern

Path und Files

Files.copy übernimmt Kopieren und Ressourcenverwaltung; Path ist die zentrale Abstraktion.

Sauberer Vergleichsgegenstand: eine Datei kopieren.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusStreams, Buffer und Close-Logik werden manuell verwaltet.Files.copy übernimmt Kopieren und Ressourcenverwaltung; Path ist die zentrale Abstraktion.
KernunterschiedNiedrige Stream-Mechanik wird durch eine präzisere Standard-API ersetzt.
PatternsAdapterAdapter
Nicht gleichsetzen: Für Streaming, spezielle Buffer oder sehr große Daten können niedrigere APIs weiterhin sinnvoll sein.

Code mit gleicher fachlicher Rolle

Gleiche Copy-Operation

package at.aydin.lab.io.legacy;

// Design Pattern: Adapter – kapselt streambasiertes Kopieren hinter einer kleinen Datei-API.
import java.io.*;

public final class LegacyFileCopy {
    public long copy(File source, File target) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        long bytes = 0;
        try {
            in = new FileInputStream(source);
            out = new FileOutputStream(target);
            byte[] buffer = new byte[4096];
            int read;
            while ((read = in.read(buffer)) >= 0) {
                out.write(buffer, 0, read);
                bytes += read;
            }
            return bytes;
        } finally {
            if (in != null) try {
                in.close();
            } catch (IOException ignored) {
            }
            if (out != null) out.close();
        }
    }
}
package at.aydin.lab.io.modern;

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;

// Design Pattern: Adapter
// Zweck: Fachliche Dateioperationen werden hinter der modernen Path-/Files-API gebündelt.
public final class PathFileService {
    public long copy(Path source, Path target) throws IOException {
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
        return Files.size(target);
    }

    public List<Path> find(Path root, String suffix) throws IOException {
        try (var paths = Files.walk(root)) {
            return paths.filter(Files::isRegularFile).filter(p -> p.getFileName().toString().endsWith(suffix)).sorted()
                    .toList();
        }
    }

    public void zip(Path source, Path zip) throws IOException {
        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(zip))) {
            out.putNextEntry(new ZipEntry(source.getFileName().toString()));
            Files.copy(source, out);
            out.closeEntry();
        }
    }
}

Empfohlene Migration

  1. Verhalten sichern: eine Datei kopieren mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: File und manuelle Streams hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Path und Files implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
18Exception Handling und LoggingNull/technische Ausnahme und verteiltes Logging → Fachliche Exception und strukturierter ServiceVergleich öffnen
Legacy

Null/technische Ausnahme und verteiltes Logging

Fehlerkontext ist technisch oder wird durch Rückgabewerte verschleiert.

Modern

Fachliche Exception und strukturierter Service

Eine benannte fachliche Exception trägt den fehlenden Schlüssel und wird zentral geloggt.

Sauberer Vergleichsgegenstand: einen Customer finden oder einen klaren Fehler liefern.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusFehlerkontext ist technisch oder wird durch Rückgabewerte verschleiert.Eine benannte fachliche Exception trägt den fehlenden Schlüssel und wird zentral geloggt.
KernunterschiedFehler werden als Teil des fachlichen Vertrags modelliert.
PatternsError Code / technische ExceptionDomain Exception, Service Layer
Nicht gleichsetzen: Exceptions sind kein Ersatz für erwartbare Ergebnisvarianten wie Optional oder Result-Typen.

Code mit gleicher fachlicher Rolle

Gleicher Customer-Lookup

package at.aydin.lab.errors.legacy;

// Design Pattern: Exception Shielding (Legacy-Antipattern-Variante) – fängt technische Fehler, verschluckt sie jedoch.
import java.util.*;

public final class LegacyCustomerLookup {
    private final Map<Long, String> data = Map.of(1L, "Aydin");
    public String find(long id) {
        try {
            return data.get(id).toUpperCase();
        } catch (Exception ex) {
            System.err.println(ex);
            return null;
        }
    }
}
package at.aydin.lab.errors.modern;

import java.util.*;

// Design Pattern: Exception Translator
// Zweck: Technische Abwesenheit wird in eine eindeutige fachliche Exception übersetzt.
public final class CustomerLookupService {
    private static final System.Logger LOG = System.getLogger(CustomerLookupService.class.getName());
    private final Map<Long, String> data = Map.of(1L, "Aydin");
    public String find(long id) {
        return Optional.ofNullable(data.get(id)).map(String::toUpperCase).orElseThrow(() -> {
            LOG.log(System.Logger.Level.WARNING, "customerId={0} not found", id); return new CustomerNotFoundException(id);
            });
    }
}

Empfohlene Migration

  1. Verhalten sichern: einen Customer finden oder einen klaren Fehler liefern mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Null/technische Ausnahme und verteiltes Logging hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Fachliche Exception und strukturierter Service implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
19JUnit und TestdesignJUnit 4 Einzeltest → JUnit 5 Parameterized TestVergleich öffnen
Legacy

JUnit 4 Einzeltest

Ein einzelner @Test deckt genau einen Beispieldatensatz ab.

Modern

JUnit 5 Parameterized Test

Ein @ParameterizedTest führt dieselbe Aussage mit mehreren repräsentativen Datensätzen aus.

Sauberer Vergleichsgegenstand: dieselbe Mehrwertsteuerberechnung testen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin einzelner @Test deckt genau einen Beispieldatensatz ab.Ein @ParameterizedTest führt dieselbe Aussage mit mehreren repräsentativen Datensätzen aus.
KernunterschiedTestdaten werden vom Testablauf getrennt und die Abdeckung wird kompakter erweitert.
PatternsExample-based TestParameterized Test
Nicht gleichsetzen: Parameterized Tests ersetzen keine gezielten Grenz-, Fehler- und Property-based Tests.

Code mit gleicher fachlicher Rolle

Die tatsächlich relevanten Testklassen

package at.aydin.lab.testing.legacy;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class VatCalculatorTest {
    @Test
    public void calculatesTwentyPercent() {
        assertEquals(120.0, new VatCalculator().gross(100, 0.2), 0.001);
    }
}
package at.aydin.lab.testing.modern;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

// Design Pattern: Parameterized Test
// Zweck: Ein fachliches Verhalten wird mit mehreren repräsentativen Datenkombinationen geprüft.
class VatCalculatorTest {
    @ParameterizedTest
    @CsvSource({
        "100,0.2,120", "50,0.1,55", "0,0.2,0"
    }) void calculates(double net, double rate, double expected) {
        assertEquals(expected, new VatCalculator().gross(net, rate));
    }
}

Empfohlene Migration

  1. Verhalten sichern: dieselbe Mehrwertsteuerberechnung testen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JUnit 4 Einzeltest hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: JUnit 5 Parameterized Test implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
20HTTP-Client und WebtestURL-Verbindung direkt im Fachcode → HttpClient hinter TextTransport-PortVergleich öffnen
Legacy

URL-Verbindung direkt im Fachcode

Netzwerkzugriff und Auswertung sind eng gekoppelt; Tests können schwer isoliert werden.

Modern

HttpClient hinter TextTransport-Port

Java HttpClient ist ein Adapter; der Service hängt nur von einem kleinen TextTransport-Port ab.

Sauberer Vergleichsgegenstand: HTML laden und ohne echtes Netzwerk testen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusNetzwerkzugriff und Auswertung sind eng gekoppelt; Tests können schwer isoliert werden.Java HttpClient ist ein Adapter; der Service hängt nur von einem kleinen TextTransport-Port ab.
KernunterschiedI/O wird an der Systemgrenze isoliert und im Test durch einen Fake ersetzt.
PatternsDirect DependencyPort and Adapter, Test Double
Nicht gleichsetzen: Der Fake-Test prüft keine TLS-, Timeout- oder HTTP-Protokolldetails; dafür bleiben Integrationstests nötig.

Code mit gleicher fachlicher Rolle

HTTP-Transport

package at.aydin.lab.http.legacy;

// Design Pattern: Adapter – kapselt HttpURLConnection hinter einer kleinen Client-API.
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public final class LegacyHttpClient {
    public String get(String url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");
        try (InputStream in = connection.getInputStream()) {
            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
        } finally {
            connection.disconnect();
        }
    }
}
package at.aydin.lab.http.modern;

import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

// Design Pattern: Adapter
// Zweck: Adaptiert java.net.http.HttpClient an den fachlichen TextTransport-Port.
public final class JavaHttpTransport implements TextTransport {
    private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
    public String get(URI uri) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(10)).GET().build();
        return client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).body();
    }
}

Teststrategie

package at.aydin.lab.http.legacy;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;

class LegacyHttpClientTest {
    @Test
    void rejectsMalformedUrl() {
        assertThrows(Exception.class, () -> new LegacyHttpClient().get("not a url"));
    }
}
package at.aydin.lab.http.modern;

import org.junit.jupiter.api.Test;
import java.net.URI;
import static org.junit.jupiter.api.Assertions.assertEquals;

class PageTitleServiceTest {
    @Test
    void testsWithoutNetwork() throws Exception {
        TextTransport fake = uri -> "<html><title>Testseite</title></html>";
        assertEquals("Testseite", new PageTitleService(fake).title(URI.create("https://example.invalid")));
    }
}

Empfohlene Migration

  1. Verhalten sichern: HTML laden und ohne echtes Netzwerk testen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: URL-Verbindung direkt im Fachcode hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: HttpClient hinter TextTransport-Port implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
21Maven Properties und KonfigurationUntypisierte java.util.Properties → Factory plus typisiertes Configuration ObjectVergleich öffnen
Legacy

Untypisierte java.util.Properties

Aufrufer lesen Strings und konvertieren Werte selbst oder über generische Getter.

Modern

Factory plus typisiertes Configuration Object

ConfigFactory definiert Prioritäten und erzeugt ein validiertes ApplicationConfig-Record.

Sauberer Vergleichsgegenstand: Port und Feature-Flag aus Properties und Environment lesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusAufrufer lesen Strings und konvertieren Werte selbst oder über generische Getter.ConfigFactory definiert Prioritäten und erzeugt ein validiertes ApplicationConfig-Record.
KernunterschiedUnstrukturierte Schlüssel/Werte werden an einer Grenze in einen typisierten Vertrag überführt.
PatternsConfiguration ObjectFactory, Typed Configuration Object
Nicht gleichsetzen: Maven Properties sind Build-Zeit-Konfiguration; Laufzeitkonfiguration sollte nicht unbemerkt in Build-Profile verlagert werden.

Code mit gleicher fachlicher Rolle

Konfiguration lesen und typisieren

package at.aydin.lab.config.legacy;

// Design Pattern: Configuration Object – bündelt untypisierte Properties in einem Zugriffspunkt.
import java.io.*;
import java.util.*;

public final class LegacyPropertiesConfig {
    private final Properties properties = new Properties();
    public LegacyPropertiesConfig(InputStream in) throws IOException {
        properties.load(in);
    }

    public String get(String key) {
        return properties.getProperty(key);
    }

    public int getInt(String key) {
        return Integer.parseInt(get(key));
    }
}
package at.aydin.lab.config.modern;

import java.util.*;

// Design Pattern: Factory
// Zweck: Zentralisiert Priorität, Parsing und Validierung verschiedener Konfigurationsquellen.
public final class ConfigFactory {
    public ApplicationConfig from(Properties p, Map<String, String> env) {
        int port = Integer.parseInt(env.getOrDefault("APP_PORT", p.getProperty("server.port", "8080")));
        boolean enabled = Boolean.parseBoolean(env.getOrDefault("FEATURE_ENABLED", p.getProperty("feature.enabled", "false")));
        return new ApplicationConfig(port, enabled);
    }
}

Modernes Zielmodell

package at.aydin.lab.config.legacy;

// Design Pattern: Configuration Object – bündelt untypisierte Properties in einem Zugriffspunkt.
import java.io.*;
import java.util.*;

public final class LegacyPropertiesConfig {
    private final Properties properties = new Properties();
    public LegacyPropertiesConfig(InputStream in) throws IOException {
        properties.load(in);
    }

    public String get(String key) {
        return properties.getProperty(key);
    }

    public int getInt(String key) {
        return Integer.parseInt(get(key));
    }
}
package at.aydin.lab.config.modern;

// Design Pattern: Configuration Object
// Zweck: Zusammengehörige Einstellungen werden typisiert und validiert gebündelt.
public record ApplicationConfig(int serverPort, boolean featureEnabled) {
    public ApplicationConfig {
        if (serverPort < 1 || serverPort > 65535) throw new IllegalArgumentException("serverPort");
    }
}

Empfohlene Migration

  1. Verhalten sichern: Port und Feature-Flag aus Properties und Environment lesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Untypisierte java.util.Properties hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Factory plus typisiertes Configuration Object implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
22Spring JDBCJdbcTemplate DAO → NamedParameterJdbcTemplate RepositoryVergleich öffnen
Legacy

JdbcTemplate DAO

Die DAO-API und positionsbasierte Parameter sind stärker technisch geprägt.

Modern

NamedParameterJdbcTemplate Repository

Das Repository verwendet benannte Parameter und ein typisiertes Mapping.

Sauberer Vergleichsgegenstand: Customer-Daten per SQL speichern und lesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDie DAO-API und positionsbasierte Parameter sind stärker technisch geprägt.Das Repository verwendet benannte Parameter und ein typisiertes Mapping.
KernunterschiedSQL bleibt sichtbar, wird aber lesbarer und hinter einer Repository-Grenze gebündelt.
PatternsDAORepository, Row Mapper
Nicht gleichsetzen: Spring JDBC ist nicht automatisch Legacy; der Vergleich zeigt zwei Qualitätsstufen innerhalb desselben Ansatzes.

Code mit gleicher fachlicher Rolle

Gleicher SQL-Datenzugriff

package at.aydin.lab.jdbc.legacy;

import java.sql.*;
import java.util.*;

// Design Pattern: DAO
// Zweck: Kapselt Statement-, ResultSet- und Mapping-Boilerplate.
public final class CustomerDao {
    private final Connection connection;
    public CustomerDao(Connection connection) {
        this.connection = connection;
    }

    public void init() throws SQLException {
        try (Statement s = connection.createStatement()) {
            s.execute("create table if not exists customer(id bigint primary key,name varchar(100))");
        }
    }

    public void save(long id, String name) throws SQLException {
        try (PreparedStatement p = connection.prepareStatement("merge into customer key(id) values(?,?)")) {
            p.setLong(1, id);
            p.setString(2, name);
            p.executeUpdate();
        }
    }

    public Optional<String> findName(long id) throws SQLException {
        try (PreparedStatement p = connection.prepareStatement("select name from customer where id=?")) {
            p.setLong(1, id);
            try (ResultSet r = p.executeQuery()) {
                return r.next() ? Optional.of(r.getString(1)) : Optional.empty();
            }
        }
    }
}
package at.aydin.lab.jdbc.modern;

import org.springframework.jdbc.core.simple.JdbcClient;
import java.util.*;

// Design Pattern: Repository + Row Mapper
// Zweck: SQL bleibt explizit, Infrastruktur-Boilerplate wird vom JdbcClient übernommen.
public final class CustomerRepository {
    private final JdbcClient jdbc;
    public CustomerRepository(JdbcClient jdbc) {
        this.jdbc = jdbc;
    }

    public void init() {
        jdbc.sql("create table if not exists customer(id bigint primary key,name varchar(100))").update();
    }

    public void save(Customer c) {
        jdbc.sql("merge into customer key(id) values(:id,:name)").param("id", c.id()).param("name", c.name())
                .update();
    }

    public Optional<Customer> find(long id) {
        return jdbc.sql("select id,name from customer where id=:id").param("id", id).query((rs, row) -> new Customer(rs.getLong("id"),
            rs.getString("name"))).optional();
    }
}

Empfohlene Migration

  1. Verhalten sichern: Customer-Daten per SQL speichern und lesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JdbcTemplate DAO hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: NamedParameterJdbcTemplate Repository implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
23Hibernate und JPANative Hibernate Session → Jakarta Persistence plus Spring Data JPAVergleich öffnen
Legacy

Native Hibernate Session

Session, Transaction und Query werden direkt über Hibernate-APIs gesteuert.

Modern

Jakarta Persistence plus Spring Data JPA

Ein JPA-Entity und Spring-Data-Repository bilden die Persistenzgrenze.

Sauberer Vergleichsgegenstand: Books persistieren und abfragen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusSession, Transaction und Query werden direkt über Hibernate-APIs gesteuert.Ein JPA-Entity und Spring-Data-Repository bilden die Persistenzgrenze.
KernunterschiedProvider-spezifische API wird durch Standard-API und generiertes Repository reduziert.
PatternsData Mapper / Unit of WorkEntity, Repository
Nicht gleichsetzen: Für komplexe Queries, Fetch-Pläne und Performanceanalyse bleibt JPA-/Hibernate-Wissen erforderlich.

Code mit gleicher fachlicher Rolle

Datenzugriff

package at.aydin.lab.hibernate.legacy;

import org.hibernate.*;
import org.hibernate.cfg.Configuration;
import java.util.*;

// Design Pattern: Unit of Work
// Zweck: Hibernate Session und Transaction bilden eine explizite Arbeitseinheit.
public final class HibernateBookStore implements AutoCloseable {
    private final SessionFactory factory = new Configuration().addAnnotatedClass(LegacyBook.class).setProperty("hibernate.connection.url",
        "jdbc:h2:mem:hibernatelegacy;DB_CLOSE_DELAY=-1").setProperty("hibernate.connection.driver_class", "org.h2.Driver")
            .setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect").setProperty("hibernate.hbm2ddl.auto", "create-drop")
            .buildSessionFactory();
    public Long save(String title) {
        try (Session s = factory.openSession()) {
            Transaction tx = s.beginTransaction();
            Long id = (Long) s.save(new LegacyBook(title));
            tx.commit();
            return id;
        }
    }

    public Optional<LegacyBook> find(Long id) {
        try (Session s = factory.openSession()) {
            return Optional.ofNullable(s.get(LegacyBook.class, id));
        }
    }

    public void close() {
        factory.close();
    }
}
package at.aydin.lab.hibernate.modern;

import org.springframework.data.jpa.repository.JpaRepository;

// Design Pattern: Repository
// Zweck: Datenzugriff wird als deklarativer fachlicher Vertrag ausgedrückt.
public interface BookRepository extends JpaRepository<BookEntity, Long> {
}

Persistenzmodell

package at.aydin.lab.hibernate.legacy;

import javax.persistence.*;

@Entity
@Table(name = "book")
public class LegacyBook {
    @Id
    @GeneratedValue
    private Long id;
    private String title;
    protected LegacyBook() {
    }

    public LegacyBook(String title) {
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }
}
package at.aydin.lab.hibernate.modern;

import jakarta.persistence.*;

@Entity
@Table(name = "book")
public class BookEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    protected BookEntity() {
    }

    public BookEntity(String title) {
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }
}

Empfohlene Migration

  1. Verhalten sichern: Books persistieren und abfragen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Native Hibernate Session hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Jakarta Persistence plus Spring Data JPA implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
24MongoDBMongo Driver, BSON Document und manuelles Mapping → Spring Data MongoDB RepositoryVergleich öffnen
Legacy

Mongo Driver, BSON Document und manuelles Mapping

Filter, Upsert, Cursor-Iteration und Document-Mapping werden manuell implementiert.

Modern

Spring Data MongoDB Repository

Spring Data leitet die Query aus dem Repository-Methodennamen ab und mappt ein @Document-Record.

Sauberer Vergleichsgegenstand: Books speichern und nach Erscheinungsjahr suchen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusFilter, Upsert, Cursor-Iteration und Document-Mapping werden manuell implementiert.Spring Data leitet die Query aus dem Repository-Methodennamen ab und mappt ein @Document-Record.
KernunterschiedDriver- und BSON-Details werden aus dem Anwendungscode entfernt.
PatternsData Mapper, RepositoryDocument, Repository
Nicht gleichsetzen: Abgeleitete Queries decken nicht jede Aggregation oder Performanceanforderung ab; MongoTemplate oder Driver bleiben bei Bedarf sinnvoll.

Code mit gleicher fachlicher Rolle

Repository für denselben Use Case

package at.aydin.lab.mongodb.legacy;

import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.ReplaceOptions;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
import static com.mongodb.client.model.Filters.gte;

// Design Pattern: Repository
// Zweck: Mongo-Driver, BSON-Filter und manuelles Mapping werden hinter einer fachlichen API gekapselt.
public final class LegacyBookRepository {
    private final MongoCollection<Document> collection;
    private final LegacyBookMapper mapper;
    public LegacyBookRepository(MongoCollection<Document> collection, LegacyBookMapper mapper) {
        this.collection = collection;
        this.mapper = mapper;
    }

    public void save(LegacyBook book) {
        collection.replaceOne(new Document("_id", book.id()), mapper.toDocument(book), new ReplaceOptions().upsert(true));
    }

    public List<LegacyBook> findByYearGreaterThanEqual(int year) {
        List<LegacyBook> result = new ArrayList<>();
        for (Document document : collection.find(gte("year", year))) {
            result.add(mapper.fromDocument(document));
        }
        return List.copyOf(result);
    }
}
package at.aydin.lab.mongodb.modern;

import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.*;

// Design Pattern: Repository
// Zweck: Query-Ableitung ersetzt manuelle Document-/Cursor-Verarbeitung.
public interface BookRepository extends MongoRepository<BookDocument, String> {
    List<BookDocument> findByYearGreaterThanEqual(int year);
}

Mapping-Modell

package at.aydin.lab.mongodb.legacy;

import org.bson.Document;

// Design Pattern: Data Mapper
// Zweck: Übersetzt manuell zwischen Fachobjekt und MongoDB Document.
public final class LegacyBookMapper {
    public Document toDocument(LegacyBook b) {
        return new Document("_id", b.id()).append("title", b.title()).append("year", b.year());
    }

    public LegacyBook fromDocument(Document d) {
        return new LegacyBook(d.getString("_id"), d.getString("title"), d.getInteger("year"));
    }
}
package at.aydin.lab.mongodb.modern;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document("books")
public record BookDocument(@Id String id, String title, int year) {
}

Empfohlene Migration

  1. Verhalten sichern: Books speichern und nach Erscheinungsjahr suchen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Mongo Driver, BSON Document und manuelles Mapping hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring Data MongoDB Repository implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
25JNDI und Service-AuflösungJNDI Service Locator → Constructor Injection über einen PortVergleich öffnen
Legacy

JNDI Service Locator

Der Use Case kennt einen String-Namen und löst den Service zur Laufzeit über Context.lookup auf.

Modern

Constructor Injection über einen Port

Der Use Case erhält GreetingPort direkt über den Konstruktor.

Sauberer Vergleichsgegenstand: denselben Greeting-Use-Case ausführen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDer Use Case kennt einen String-Namen und löst den Service zur Laufzeit über Context.lookup auf.Der Use Case erhält GreetingPort direkt über den Konstruktor.
KernunterschiedString-basierte Infrastrukturabhängigkeit wird zu einem compile-time sichtbaren Vertrag.
PatternsService LocatorPort, Constructor Injection
Nicht gleichsetzen: JNDI bleibt für bestimmte Serverressourcen sinnvoll; problematisch ist vor allem der direkte Lookup im Fachcode.

Code mit gleicher fachlicher Rolle

Gleicher Greeting-Use-Case

package at.aydin.lab.jndi.legacy;

// Design Pattern: Service Locator
// Zweck: Zeigt die versteckte Laufzeitabhängigkeit auf JNDI-Name und Naming-Infrastruktur.
public final class LegacyGreetingUseCase {
    private final JndiServiceLocator locator;
    public LegacyGreetingUseCase(JndiServiceLocator locator) {
        this.locator = locator;
    }

    public String execute(String name) {
        GreetingPort port = locator.resolve("service/greeting", GreetingPort.class);
        return port.greet(name);
    }
}
package at.aydin.lab.jndi.modern;

import org.springframework.stereotype.Component;

@Component
public class GreetingUseCase {
    private final GreetingPort port;
    public GreetingUseCase(GreetingPort port) {
        this.port = port;
    }

    public String execute(String name) {
        return port.greet(name);
    }
}

Gleiche Service-Implementierung

package at.aydin.lab.jndi.legacy;

public final class GreetingService implements GreetingPort {
    @Override
    public String greet(String name) {
        return "Hallo " + name;
    }
}
package at.aydin.lab.jndi.modern;

import org.springframework.stereotype.Service;

@Service
public class GreetingService implements GreetingPort {
    public String greet(String name) {
        return "Hallo " + name;
    }
}

Empfohlene Migration

  1. Verhalten sichern: denselben Greeting-Use-Case ausführen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JNDI Service Locator hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Constructor Injection über einen Port implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
26JMX und MonitoringJMX Standard MBean → Micrometer CounterVergleich öffnen
Legacy

JMX Standard MBean

Ein MBean wird unter einem ObjectName im Platform MBeanServer registriert und über JMX abgefragt.

Modern

Micrometer Counter

Ein Micrometer Counter wird in einer MeterRegistry registriert und kann über Actuator/Monitoring-Backends exportiert werden.

Sauberer Vergleichsgegenstand: abgeschlossene Tasks als Betriebsmetrik zählen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin MBean wird unter einem ObjectName im Platform MBeanServer registriert und über JMX abgefragt.Ein Micrometer Counter wird in einer MeterRegistry registriert und kann über Actuator/Monitoring-Backends exportiert werden.
KernunterschiedManagement-API und Exportformat werden von JMX-spezifisch zu vendor-neutralen Metriken verschoben.
PatternsManagement FacadeObserver, Metrics Facade
Nicht gleichsetzen: JMX bietet auch Operationen und komplexe Management-Objekte; ein Counter ersetzt nicht jede MBean-Funktion.

Code mit gleicher fachlicher Rolle

Gleiche Task-Metrik

package at.aydin.lab.jmx.legacy;

import java.util.concurrent.atomic.AtomicLong;

// Design Pattern: Management Facade
// Zweck: Beschränkt JMX-Zugriff auf kontrollierte Monitoring-Operationen.
public final class TaskCounter implements TaskCounterMBean {
    private final AtomicLong completed = new AtomicLong();
    public void taskCompleted() {
        completed.incrementAndGet();
    }

    public long getCompleted() {
        return completed.get();
    }

    public void reset() {
        completed.set(0);
    }
}
package at.aydin.lab.jmx.modern;

import io.micrometer.core.instrument.*;
import org.springframework.stereotype.Component;

// Design Pattern: Observer
// Zweck: Fachliche Ereignisse aktualisieren eine entkoppelte Metrik für Actuator/Monitoring-Systeme.
@Component
public class TaskMetrics {
    private final Counter completed;
    public TaskMetrics(MeterRegistry registry) {
        completed = Counter.builder("tasks.completed").description("Completed tasks").register(registry);
    }

    public void taskCompleted() {
        completed.increment();
    }

    public double count() {
        return completed.count();
    }
}

Gleiche Zählprüfung

package at.aydin.lab.jmx.legacy;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class TaskCounterTest {
    @Test
    void counts() {
        var c = new TaskCounter();
        c.taskCompleted();
        assertEquals(1, c.getCompleted());
    }
}
package at.aydin.lab.jmx.modern;

import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class TaskMetricsTest {
    @Test
    void publishesMetric() {
        var m = new TaskMetrics(new SimpleMeterRegistry());
        m.taskCompleted();
        assertEquals(1, m.count());
    }
}

Empfohlene Migration

  1. Verhalten sichern: abgeschlossene Tasks als Betriebsmetrik zählen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JMX Standard MBean hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Micrometer Counter implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
27EJB und ServicesEJB 2 Session Bean mit Home/Remote Interfaces → Spring Service mit Constructor InjectionVergleich öffnen
Legacy

EJB 2 Session Bean mit Home/Remote Interfaces

Lifecycle, Remote-Vertrag und Lookup werden über EJB-2-Interfaces und Containerkonventionen modelliert.

Modern

Spring Service mit Constructor Injection

Ein POJO-Service trägt die Fachlogik; ein separater Controller kann den Transport übernehmen.

Sauberer Vergleichsgegenstand: eine Order-Funktion als Enterprise-Service bereitstellen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusLifecycle, Remote-Vertrag und Lookup werden über EJB-2-Interfaces und Containerkonventionen modelliert.Ein POJO-Service trägt die Fachlogik; ein separater Controller kann den Transport übernehmen.
KernunterschiedContainer- und Remote-Boilerplate wird von der Fachlogik getrennt.
PatternsSession Facade, Home/RemoteService Layer, Constructor Injection
Nicht gleichsetzen: Remote-Aufrufe haben andere Fehler- und Transaktionssemantiken als lokale Spring-Bean-Aufrufe; eine 1:1-Ersetzung ist nicht immer möglich.

Code mit gleicher fachlicher Rolle

Enterprise-Service

package at.aydin.lab.ejb.legacy;

import javax.ejb.*;

// Design Pattern: Session Facade
// Zweck: Bündelt fachlichen Bestellablauf in einer serverseitigen Session-Komponente.
public class OrderSessionBean implements SessionBean {
    public String placeOrder(String sku, int quantity) {
        return "ORDERED " + quantity + " x " + sku;
    }

    public void ejbCreate() {
    }

    public void ejbActivate() {
    }

    public void ejbPassivate() {
    }

    public void ejbRemove() {
    }

    public void setSessionContext(SessionContext context) {
    }
}
package at.aydin.lab.ejb.modern;

import org.springframework.stereotype.Service;

// Design Pattern: Service Layer
// Zweck: Fachlogik ist containerleicht, lokal testbar und nicht an RemoteObject gebunden.
@Service
public class OrderService {
    public String placeOrder(String sku, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("quantity");
        return "ORDERED " + quantity + " x " + sku;
    }
}

Empfohlene Migration

  1. Verhalten sichern: eine Order-Funktion als Enterprise-Service bereitstellen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: EJB 2 Session Bean mit Home/Remote Interfaces hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Spring Service mit Constructor Injection implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
28JPMS / JigsawClasspath-Plugin per Class.forName → JPMS ServiceLoader mit uses/providesVergleich öffnen
Legacy

Classpath-Plugin per Class.forName

Der Klassenname ist ein String; Sichtbarkeit und Abhängigkeiten sind global auf dem Classpath.

Modern

JPMS ServiceLoader mit uses/provides

Ein exportiertes SPI, module-info und ServiceLoader definieren Consumer und Provider explizit.

Sauberer Vergleichsgegenstand: einen Greeting-Provider entdecken und ausführen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDer Klassenname ist ein String; Sichtbarkeit und Abhängigkeiten sind global auf dem Classpath.Ein exportiertes SPI, module-info und ServiceLoader definieren Consumer und Provider explizit.
KernunterschiedString-basierte Reflection wird durch ein modulbeschriebenes Service-Provider-Modell ersetzt.
PatternsPlugin Loader, ReflectionService Provider Interface, Module Descriptor
Nicht gleichsetzen: JPMS bietet starke Kapselung, aber keinen dynamischen Bundle-Lifecycle wie OSGi.

Code mit gleicher fachlicher Rolle

Provider laden

package at.aydin.lab.jpms.legacy;

// Design Pattern: Plugin Loader / Factory – erzeugt Plugins dynamisch über Reflection.
public final class LegacyPluginLoader {
    public Object load(String className) {
        try {
            return Class.forName(className).getDeclaredConstructor().newInstance();
        } catch (ReflectiveOperationException ex) {
            throw new IllegalStateException(ex);
        }
    }
}
package at.aydin.lab.jpms.modern;

import at.aydin.lab.jpms.modern.api.GreetingProvider;
import java.util.*;

public final class JpmsApplication {
    private JpmsApplication() {
    }

    public static void main(String[] args) {
        GreetingProvider provider = ServiceLoader.load(GreetingProvider.class).findFirst().orElseThrow();
        System.out.println(provider.greet("Aydin"));
    }
}

Provider-Implementierung

package at.aydin.lab.jpms.legacy;

public final class LegacyGreetingPlugin {
    public String greet() {
        return "Hallo aus dem Classpath";
    }
}
package at.aydin.lab.jpms.modern.internal;

import at.aydin.lab.jpms.modern.api.GreetingProvider;

public final class GermanGreetingProvider implements GreetingProvider {
    public String greet(String name) {
        return "Hallo " + name;
    }
}

Empfohlene Migration

  1. Verhalten sichern: einen Greeting-Provider entdecken und ausführen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Classpath-Plugin per Class.forName hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: JPMS ServiceLoader mit uses/provides implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
29Service Registry und ServiceLoaderEigene dynamische Service Registry → Java SPI mit ServiceLoaderVergleich öffnen
Legacy

Eigene dynamische Service Registry

Services werden zur Laufzeit explizit registriert, ersetzt und entfernt.

Modern

Java SPI mit ServiceLoader

Provider werden über META-INF/services beziehungsweise Modulmetadaten entdeckt.

Sauberer Vergleichsgegenstand: einen Payment-Service finden und verwenden.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusServices werden zur Laufzeit explizit registriert, ersetzt und entfernt.Provider werden über META-INF/services beziehungsweise Modulmetadaten entdeckt.
KernunterschiedEine eigene mutable Registry wird durch den Java-Standard für Provider Discovery ersetzt.
PatternsService RegistryService Provider Interface, ServiceLoader
Nicht gleichsetzen: Dieses Projekt ist ausdrücklich kein echtes OSGi-Beispiel: ServiceLoader besitzt keinen OSGi-Bundle-Lifecycle, keine Versionierung und keine dynamische Registry-Semantik.

Code mit gleicher fachlicher Rolle

Service-Auflösung

package at.aydin.lab.osgi.legacy;

import java.util.*;

// Design Pattern: Service Registry
// Zweck: Dienste können dynamisch registriert, ersetzt und entfernt werden.
public final class ServiceRegistry {
    private final Map<Class<?>, Object> services = new HashMap<>();
    public <T> void register(Class<T> type, T service) {
        services.put(type, service);
    }

    public void unregister(Class<?> type) {
        services.remove(type);
    }

    public <T> Optional<T> find(Class<T> type) {
        return Optional.ofNullable(type.cast(services.get(type)));
    }
}
package at.aydin.lab.osgi.modern;

import java.util.*;

public final class PaymentProviders {
    public PaymentProvider first() {
        return ServiceLoader.load(PaymentProvider.class).findFirst().orElseThrow();
    }
}

Service-Vertrag

package at.aydin.lab.osgi.legacy;

public interface PaymentService {
    String pay(int cents);
}
package at.aydin.lab.osgi.modern;

// Design Pattern: Service Provider Interface
// Zweck: Provider werden ohne zentrale Registry über Standard-Java-Metadaten entdeckt.
public interface PaymentProvider {
    String pay(int cents);
}

Empfohlene Migration

  1. Verhalten sichern: einen Payment-Service finden und verwenden mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: Eigene dynamische Service Registry hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Java SPI mit ServiceLoader implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
30XML ParserDOM Parser → StAX Streaming ParserVergleich öffnen
Legacy

DOM Parser

Das gesamte XML-Dokument wird als Baum in den Speicher geladen.

Modern

StAX Streaming Parser

Der Parser liest Ereignisse vorwärts und hält nur den aktuellen Zustand.

Sauberer Vergleichsgegenstand: Order-Daten aus XML lesen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusDas gesamte XML-Dokument wird als Baum in den Speicher geladen.Der Parser liest Ereignisse vorwärts und hält nur den aktuellen Zustand.
KernunterschiedSpeicherverbrauch und Streaming-Fähigkeit verbessern sich bei großen Dokumenten.
PatternsTree ParserStreaming Parser
Nicht gleichsetzen: DOM ist für kleine Dokumente und freie Navigation oft einfacher; StAX ist nicht pauschal besser.

Code mit gleicher fachlicher Rolle

Gleicher XML-Read-Use-Case

package at.aydin.lab.xmlparser.legacy;

import javax.xml.parsers.*;
import java.io.*;

// Design Pattern: Parser
// Zweck: DOM bietet komfortablen Objektbaum, lädt aber das komplette Dokument.
public final class DomOrderReader {
    public double total(InputStream in) throws Exception {
        var f = DocumentBuilderFactory.newInstance();
        f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        var nodes = f.newDocumentBuilder().parse(in).getElementsByTagName("total");
        double sum = 0;
        for (int i = 0; i < nodes.getLength(); i++) sum += Double.parseDouble(nodes.item(i).getTextContent());
        return sum;
    }
}
package at.aydin.lab.xmlparser.modern;

import java.io.InputStream;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;

// Design Pattern: Iterator
// Zweck: Pull-Parsing verarbeitet große Dokumente ereignisweise mit konstantem Speicherbedarf.
public final class StaxOrderReader {
    public double total(InputStream inputStream) throws Exception {
        XMLInputFactory factory = XMLInputFactory.newFactory();
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);

        XMLStreamReader reader = factory.createXMLStreamReader(inputStream);
        double sum = 0;
        while (reader.hasNext()) {
            boolean totalElement = reader.next() == XMLStreamConstants.START_ELEMENT
                    && reader.getLocalName().equals("total");
            if (totalElement) {
                sum += Double.parseDouble(reader.getElementText());
            }
        }
        reader.close();
        return sum;
    }
}

Empfohlene Migration

  1. Verhalten sichern: Order-Daten aus XML lesen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: DOM Parser hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: StAX Streaming Parser implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
31XSLTTransformerFactory ohne Hardening → Vorkompilierte Templates mit Secure ProcessingVergleich öffnen
Legacy

TransformerFactory ohne Hardening

Stylesheet wird pro Aufruf verarbeitet; externe Ressourcen können je nach Factory-Konfiguration erreichbar sein.

Modern

Vorkompilierte Templates mit Secure Processing

Templates werden einmal kompiliert und externe DTD-/Stylesheet-Zugriffe werden eingeschränkt.

Sauberer Vergleichsgegenstand: XML mit XSLT transformieren.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusStylesheet wird pro Aufruf verarbeitet; externe Ressourcen können je nach Factory-Konfiguration erreichbar sein.Templates werden einmal kompiliert und externe DTD-/Stylesheet-Zugriffe werden eingeschränkt.
KernunterschiedPerformance und Sicherheitsgrenzen werden explizit gemacht.
PatternsTransformerTemplate, Secure Configuration
Nicht gleichsetzen: Secure Processing allein ersetzt keine vertrauenswürdige Stylesheet-Quelle und keine Output-Validierung.

Code mit gleicher fachlicher Rolle

Gleiche Transformation

package at.aydin.lab.xslt.legacy;

// Design Pattern: Factory – erzeugt Transformer über die standardisierte TransformerFactory.
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;

public final class LegacyXsltTransformer {
    public String transform(InputStream xml, InputStream xsl) throws Exception {
        StringWriter out = new StringWriter();
        TransformerFactory.newInstance().newTransformer(new StreamSource(xsl)).transform(new StreamSource(xml), new StreamResult(out));
        return out.toString();
    }
}
package at.aydin.lab.xslt.modern;

import javax.xml.XMLConstants;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;

// Design Pattern: Flyweight
// Zweck: Thread-sichere Templates werden einmal kompiliert und für viele Transformationen wiederverwendet.
public final class SecureXsltTemplate {
    private final Templates templates;
    public SecureXsltTemplate(InputStream xsl) throws Exception {
        TransformerFactory f = TransformerFactory.newInstance();
        f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        f.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
        f.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
        templates = f.newTemplates(new StreamSource(xsl));
    }

    public String transform(InputStream xml) throws Exception {
        StringWriter out = new StringWriter();
        templates.newTransformer().transform(new StreamSource(xml), new StreamResult(out));
        return out.toString();
    }
}

Empfohlene Migration

  1. Verhalten sichern: XML mit XSLT transformieren mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: TransformerFactory ohne Hardening hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Vorkompilierte Templates mit Secure Processing implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
32KryptografieAES/ECB ohne Authentifizierung → AES/GCM mit zufälligem NonceVergleich öffnen
Legacy

AES/ECB ohne Authentifizierung

ECB verschlüsselt gleiche Klartextblöcke identisch und erkennt Manipulation nicht.

Modern

AES/GCM mit zufälligem Nonce

GCM liefert Vertraulichkeit und Integrität; ein zufälliger Nonce wird mit dem Ciphertext gespeichert.

Sauberer Vergleichsgegenstand: Daten symmetrisch verschlüsseln und entschlüsseln.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusECB verschlüsselt gleiche Klartextblöcke identisch und erkennt Manipulation nicht.GCM liefert Vertraulichkeit und Integrität; ein zufälliger Nonce wird mit dem Ciphertext gespeichert.
KernunterschiedUnsicherer Betriebsmodus wird durch authenticated encryption ersetzt.
PatternsCrypto FacadeAuthenticated Encryption Service
Nicht gleichsetzen: Nonce-Wiederverwendung mit demselben Schlüssel ist bei GCM kritisch; Schlüsselmanagement bleibt außerhalb des Beispiels.

Code mit gleicher fachlicher Rolle

Gleiche Verschlüsselungsaufgabe

package at.aydin.lab.crypto.legacy;

// Design Pattern: Security Gateway (Legacy-Antipattern) – bündelt Kryptografie, verwendet absichtlich unsicheres ECB.
import javax.crypto.*;
import javax.crypto.spec.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.*;

/** Lern-Antipattern: AES/ECB ist deterministisch und für produktive Daten ungeeignet. */
public final class LegacyAesEcb {
    private SecretKey key(String password) throws Exception {
        return new SecretKeySpec(Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(password.getBytes(StandardCharsets.UTF_8)),
            16), "AES");
    }

    public String encrypt(String text, String password) throws Exception {
        Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
        c.init(Cipher.ENCRYPT_MODE, key(password));
        return Base64.getEncoder().encodeToString(c.doFinal(text.getBytes(StandardCharsets.UTF_8)));
    }

    public String decrypt(String value, String password) throws Exception {
        Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
        c.init(Cipher.DECRYPT_MODE, key(password));
        return new String(c.doFinal(Base64.getDecoder().decode(value)), StandardCharsets.UTF_8);
    }
}
package at.aydin.lab.crypto.modern;

import javax.crypto.*;
import javax.crypto.spec.*;
import java.nio.charset.StandardCharsets;
import java.security.*;

// Design Pattern: Security Gateway
// Zweck: Sichere kryptografische Parameter werden zentral erzwungen.
public final class AesGcmService {
    private final SecureRandom random = new SecureRandom();
    private SecretKey key(char[] password, byte[] salt) throws Exception {
        var f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        return new SecretKeySpec(f.generateSecret(new PBEKeySpec(password, salt, 120_000, 256)).getEncoded(), "AES");
    }

    public EncryptedPayload encrypt(String text, char[] password) throws Exception {
        byte[] salt = new byte[16], iv = new byte[12];
        random.nextBytes(salt);
        random.nextBytes(iv);
        Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
        c.init(Cipher.ENCRYPT_MODE, key(password, salt), new GCMParameterSpec(128, iv));
        return new EncryptedPayload(salt, iv, c.doFinal(text.getBytes(StandardCharsets.UTF_8)));
    }

    public String decrypt(EncryptedPayload p, char[] password) throws Exception {
        Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
        c.init(Cipher.DECRYPT_MODE, key(password, p.salt()), new GCMParameterSpec(128, p.iv()));
        return new String(c.doFinal(p.cipherText()), StandardCharsets.UTF_8);
    }
}

Empfohlene Migration

  1. Verhalten sichern: Daten symmetrisch verschlüsseln und entschlüsseln mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: AES/ECB ohne Authentifizierung hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: AES/GCM mit zufälligem Nonce implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
33SMTP und MailJavaMail javax.mail Factory → Jakarta Mail Gateway plus CommandVergleich öffnen
Legacy

JavaMail javax.mail Factory

Parameter werden einzeln an eine Factory übergeben; die API verwendet javax.mail.

Modern

Jakarta Mail Gateway plus Command

Ein unveränderlicher MailCommand beschreibt den Auftrag; das Gateway kapselt Jakarta Mail und Transport.send.

Sauberer Vergleichsgegenstand: eine UTF-8-Mail vorbereiten und optional versenden.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusParameter werden einzeln an eine Factory übergeben; die API verwendet javax.mail.Ein unveränderlicher MailCommand beschreibt den Auftrag; das Gateway kapselt Jakarta Mail und Transport.send.
KernunterschiedNamespace-Migration wird mit einer klaren Anwendungsgrenze kombiniert.
PatternsFactoryCommand, Gateway
Nicht gleichsetzen: Der Unit-Test bereitet nur die Nachricht vor; tatsächlicher SMTP-Versand benötigt einen Integrationstest mit Testserver.

Code mit gleicher fachlicher Rolle

Mail vorbereiten

package at.aydin.lab.mail.legacy;

// Design Pattern: Factory – erzeugt vorkonfigurierte MimeMessage-Objekte.
import javax.mail.*;
import javax.mail.internet.*;
import java.util.*;

public final class LegacyMailFactory {
    public MimeMessage create(String from, String to, String subject, String body) throws Exception {
        Session session = Session.getDefaultInstance(new Properties());
        MimeMessage m = new MimeMessage(session);
        m.setFrom(new InternetAddress(from));
        m.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        m.setSubject(subject, "UTF-8");
        m.setText(body, "UTF-8");
        return m;
    }
}
package at.aydin.lab.mail.modern;

import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.util.*;

// Design Pattern: Gateway
// Zweck: Jakarta-Mail-API bleibt außerhalb des fachlichen Aufrufers.
public final class JakartaMailGateway {
    private final Session session;
    public JakartaMailGateway(Properties properties) {
        session = Session.getInstance(properties);
    }

    public MimeMessage prepare(MailCommand c) throws Exception {
        MimeMessage m = new MimeMessage(session);
        m.setFrom(new InternetAddress(c.from()));
        m.setRecipient(Message.RecipientType.TO, new InternetAddress(c.to()));
        m.setSubject(c.subject(), "UTF-8");
        m.setText(c.body(), "UTF-8");
        return m;
    }

    public void send(MailCommand c) throws Exception {
        Transport.send(prepare(c));
    }
}

Empfohlene Migration

  1. Verhalten sichern: eine UTF-8-Mail vorbereiten und optional versenden mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: JavaMail javax.mail Factory hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Jakarta Mail Gateway plus Command implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
34Patterns und RefactoringIf-Kette über String-Kundentypen → Strategy plus FactoryVergleich öffnen
Legacy

If-Kette über String-Kundentypen

Ein zentraler Conditional Block enthält Auswahl und Algorithmen; ungültige Werte sind freie Strings.

Modern

Strategy plus Factory

CustomerType ist typisiert, Strategien sind austauschbar und eine Factory übernimmt die Auswahl.

Sauberer Vergleichsgegenstand: den Preis für Standard-, Premium- und Employee-Kunden berechnen.

Fachlich korrekter Direktvergleich

DimensionLegacyModern
MechanismusEin zentraler Conditional Block enthält Auswahl und Algorithmen; ungültige Werte sind freie Strings.CustomerType ist typisiert, Strategien sind austauschbar und eine Factory übernimmt die Auswahl.
KernunterschiedVariation wird aus der Bedingungskette in explizite Objekte/Funktionen verschoben.
PatternsConditional Strategy AntipatternStrategy, Factory
Nicht gleichsetzen: Für drei stabile Fälle kann eine einfache switch expression verständlicher sein; Patterns müssen Komplexität rechtfertigen.

Code mit gleicher fachlicher Rolle

Fachlicher Preisaufruf

package at.aydin.lab.patterns.legacy;

// Design Pattern: Conditional Strategy (Legacy-Antipattern) – wählt Preisalgorithmen über Bedingungen.
public final class LegacyPriceCalculator {
    public double price(String customerType, double base) {
        if ("STANDARD".equals(customerType)) return base;
        if ("PREMIUM".equals(customerType)) return base * 0.9;
        if ("EMPLOYEE".equals(customerType)) return base * 0.7;
        throw new IllegalArgumentException(customerType);
    }
}
package at.aydin.lab.patterns.modern;

public final class PriceService {
    private final PricingStrategyFactory factory;
    public PriceService(PricingStrategyFactory factory) {
        this.factory = factory;
    }

    public double price(CustomerType type, double base) {
        return factory.forType(type).apply(base);
    }
}

Auswahl der Algorithmen

package at.aydin.lab.patterns.legacy;

// Design Pattern: Conditional Strategy (Legacy-Antipattern) – wählt Preisalgorithmen über Bedingungen.
public final class LegacyPriceCalculator {
    public double price(String customerType, double base) {
        if ("STANDARD".equals(customerType)) return base;
        if ("PREMIUM".equals(customerType)) return base * 0.9;
        if ("EMPLOYEE".equals(customerType)) return base * 0.7;
        throw new IllegalArgumentException(customerType);
    }
}
package at.aydin.lab.patterns.modern;

import java.util.*;

// Design Pattern: Factory
// Zweck: Auswahl konkreter Strategies wird an einer Stelle konfiguriert.
public final class PricingStrategyFactory {
    private final Map<CustomerType, PricingStrategy> strategies = Map.of(CustomerType.STANDARD, p -> p, CustomerType.PREMIUM,
        p -> p * 0.9, CustomerType.EMPLOYEE, p -> p * 0.7);
    public PricingStrategy forType(CustomerType type) {
        return Optional.ofNullable(strategies.get(type)).orElseThrow();
    }
}

Empfohlene Migration

  1. Verhalten sichern: den Preis für Standard-, Premium- und Employee-Kunden berechnen mit denselben Eingaben und erwarteten Ausgaben testen.
  2. Legacy-Grenze isolieren: If-Kette über String-Kundentypen hinter einem kleinen Vertrag oder Adapter kapseln.
  3. Modernes Gegenstück einführen: Strategy plus Factory implementieren, ohne den Fachfall zu ändern.
  4. Parity prüfen: Legacy- und Modern-Modul gegen dieselbe Fachfall-Matrix ausführen.
  5. Technische Unterschiede separat testen: Container, Protokoll, Persistenz oder Security nicht mit Unit-Tests vortäuschen.
⌂ Cockpit