2.3.2017 - asynchroner code

Asynchrones Programmieren erhält in Zeiten der Web-Apps immer grössere Bedeutung. Nehmen wir folgende, unschuldig aussehende Funktion, die irgendwelche Daten holt und zurückliefert (Pseudocode):

  function fetchData(){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      return a
    }

    Hauptprogamm(){
      coolData=fetchData()
      display(coolData)
      /* Mach weiter, update UI */
    }

So weit, so einfach und klar. Das funktioniert super, wenn fetchSomething() ausreichend schnell ist. Doch was, wenn diese Zugriffe über eine langsame Internet-Verbindung gehen? Dann scheint das Hauptprogramm bei fetchData() für den Anwender eingefroren zu sein, während es auf die Anlieferung der Daten wartet.

Um das zu verhindern, entwickelte man die Technik der asynchronen Programmierung. Da JavaScript als klassische Webapp-Sprache, die typischerweise single threaded im Browser abläuft, besonders stark mit diesem Problem konfrontiert ist, ist asynchrone Programmierung in JavaScipt auch am weitesten Verbreitet.

Wir schreiben den Code also so um:

    function fetchData(callback){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      callback(undefined, a)
      /* bzw, wenn ein Fehler passiert wäre; */
      callback(error,undefined)
    }

    Hauptprogramm(){
      fetchData(doDisplay)
      /* erledige andere Dinge, halte das UI am Leben */
    }

    function doDisplay(error,coolData){
      if(error){
        // Ooops!
      }else{
        display(coolData)
      }
    }

Viel besser. Die Funktion fetchData() kehrt sofort zurück und das Hauptprogramm kann normal weiter laufen. Wenn das Resultat da ist, wird die beim Aufruf angegebene callback-Funktion “doDisplay” mit dem Resultat aufgerufen und die Anwendung kann sich dann um die Wieterverarbeitung kümmern. Per Konvention ist bei diesen “NodeJS-Style” Callbacks der erste Parameter immer ein Fehler-Objekt oder eine Fehlermeldung.

Man kann das mit anonymen Funktionen noch ein wenig übersichtlicher gestalten:

    Hauptprogramm(){
      fetchData(function(error,coolData){
        if(!error){
          display(coolData)
        }
      })
    }

Das ist funktional exakt dasselbe, aber einfacher lesbar. Und mit dieser Programmiertechnik haben Web-Entwickler seit vielen Jahren problemlos das Web 2.0 aufgebaut. Allerdings wurden mit zunehmender Komplexität der Web-Apps auch die Klagen über die “Callback-Hölle” immer lauter. Was ist damit gemeint?

Nehmen wir mal an, wir benötigen nicht nur ein, sondern mehrere externe Datenquellen zum Aufbau unserer Anzeige. Nennen wir dieObjekte, die wir benötigen: A, B und C. Das würden wir so programmieren:

       function fetchABC(callback){
        fetchA(function(A){
          /* Irgendwelche Zwischenschritte */
          fetchB(function(B){
            /* irgendwelche Zwischenschritte */
            /* oops da ist ein Fehler passiert */
            fetchC(function(C){
              /* Aufbereitung der Daten */
              callback(coolResult)
              })
            })
          })
       }

      Hauptprogramm(){
        fetchABC(function(error,coolData){
            display(coolData)
          })
        }

Es gibt hier zwei Probleme:

  • Der Code wird zu Spaghetticode und zunehemd schlechter lesbar

  • Fehlerbehandlung wird schwierig. Was soll man mit dem Fehler anstellen, der in irgendeiner der Verschachtelungen auftritt? Mit der Behandlung des Fehlers auf jeder Stufe wird der Code noch schlechter lesbar. Häufig gelingt das auch nicht korrekt, und das Programm stürzt einfach ab, wenn tief unten in der Verschachtelung ein Fehler auftritt.

Die aktuelle JavaScript Version, die von neueren Chrome- und Firefox Browsern auch schon unterstützt wird, bringt Abhilfe. Zuerst noch zur Klärung: Diese JavaScript Version hört auf den Namen ES 6, aber auch auf den Namen ES 2015, ECMA6 und ECMA 2015, was manchmal ein wenig verwirrlich ist.

Wie auch immer. Die Idee ist: Anstatt die länger dauernde Aufgabe zu lösen, und dann ein Callback mit der Lösung aufzurufen, liefern wir sofort ein Objekt zurück, welches nicht die Lösung ist, sondern welches verspricht, die Lösung irgendwann zu liefern.

      Hauptprogramm(){
        fetchABC().then(coolData=>{
            display(coolData)
          }).catch(error=>{
            display("oops: "+error)
            })
      }

fetchABC() hat eine Promise zurückgeliefert (wir kommen weiter unten darauf, wie das geht), und der Programmteil nach “then” ist die Einlösung des Versprechens. Elegant ist der “catch”-Teil: Wenn das Versprechen nicht erfüllt werden konnte (ja, liebe Kinder, Versprechen werden manchmal gebrochen!), dann wird dieser Teil ausgeführt. Das Schöne ist, egal in welcher Verschachtelungstiefe dir Ursache war, das Programm gelangt immer an diese Stelle zur Fehlerbehandlung.

Wie wurde nun also diese Promise hergestellt? Wir brauchen drei Datenelemente A, B und C, welche von verschiedenen langsamen, aber voneinander unabhängigen Quellen stammen. Da die Quellen unabhängig sind, können wir alle drei Abfragen quasi-gleichzeitig abschicken, wenn wir nur sicherstellen, dass wir alle drei Resultate haben, bevor wir weiter machen. Das war mit den bisher gezeigten Techniken nicht so einfach zu machen. Jetzt ist es fast trivial:

      function fetchABC(){
        promiseA=fetchA()
        promiseB=fetchB()
        promiseC=fetchC()
        return Promise.all([promiseA,promiseB,promiseC]).then(resolve=>{
          /* Allfällig Verarbeitungsschritte */
          })
      }

Die Funktion gibt eine Promise zurück, welche erst dann eingelöst wird, wenn alle drei Vorbedingungs-Promises erfüllt sind.
Das ist alles.

Wirklich schön wird es nun mit dem nächsten Schritt, der im Grund nur eine Syntaxänderung ist, aber asynchrone Programmierung gleich einfach macht, wie synchrone Programmierung:

      async function fetch_and_show(){
        let coolData=await fetchABC()
        display(coolData)
      }

Hinter den Kulissen wird hier immer noch mit Promises hantiert, aber die ganzen komplizierten Konstrukte sind aus dem Quellcode verschwunden. Die Schlüsselwörter async und await können mit Typescript ab Version 2 und mit JavaScript ab Version ECMA6 eingesetzt werden.

Ach ja, aufmerksame Leser/innen werden vielleicht monieren, dass ich eigentlich immer noch nicht gezeigt habe, wie man eine Promise herstellt, zum Beispiel die oben erwähnte PromiseA.

Man kann das auf drei verschiedene Arten tun:

1.: Trivial:

Die Funktion ruft ihrerseits eine Funktion auf, die eine Promise zurückliefert, zum Beispiel eine Funktion eines asynchrones, Promise-enabled Datenbank- oder HTTP-Treibers.

2.: Promise explizit erstellen:

    function fetchA(){
        return new Promise((resolve,reject)=>{
            let result=getTheDataFromRemoteWhichtakesSomeTime()
            if(allesHatGeklappt){
                resolve(result)
            }else{
                reject(new Error("Ups, da ging was schief")
            }
        })
    }

Wir erzeugen eine neue Promise, die Methoden für die Erfüllung oder den Bruch des Versprechens enthält und liefern diese Promise sofort zurück.

3.: Promise implizit erstellen

    async function fetchA(){
        return getTheDataFromRemoteWhichtakesSomeTime()
    }

Jede mit “async” gekennzeichnete Funktion liefert implizit eine Promise zurück. Die Promise hat -ebenfalls implizit- einen resolve-Zweig, der den deklarierten return-Wert der Funktion liefert, und einen reject-Zweig, der bei jeder während der Ausführung geworfenen Exception mit diesem Exception Object aufgerufen wird.

17.9.2016 - Btrfs

Einleitung

Vor einiger Zeit habe ich ja über das ZFS-Dateisystem referiert. Das funktioniert so weit sehr gut, aber es gibt einen Nachteil: Wenn an sich das System zerschossen hat, ist das nicht schlimm, denn man hat ja ein Backup von gestern auf dem ZFS-Raid, nicht wahr?

Ja, aber leider war man (d.h.: ich) nicht vorausblickend genug, um ein entsprechende Rettungssystem zusammenzustellen. Das ist immer noch nicht schlimm, man kann Linux ja jederzeit von einem für praktisch alle Distributionen erhältlichen Live-System von Stick oder CD booten.

Tja, der kleine Nachteil ist: Diese Live-Systeme enthalten idR keine ZFS-Treiber. Das heisst, man muss zuerst aus dem Live-System ein Arbeitssystem installieren, dort ZFS installieren, dann die Backup_Disks mounten und schliesslich das Backup herüberkopieren. Vermutlich gibt es auch einen einfacheren Weg, aber den habe ich nicht gefunden.

Andererseits existiert ja Btrfs, dessen grösste Nachteile (1) der sperrige Name und (2) der ewige Beta-Status sind. Btrfs (ob Sie es lieber “Butter-eff-ess”, “Better-eff-ess” oder “Beh Teh Err Eff Ess” aussprechen wollen, sei Ihnen überlassen), ist ganz offensichtlich von ZFS inspiriert. Sein Hautpvorteil ist: Ungeachtet des ewigen Beta-Status ist es inzwischen bei praktisch allen Distributionen Teil des Kernsystems und damit auch der Live-Medien. Man kann auf Btrs-Disks also sofort nach dem Booten des Systems zugreifen.

Installation

Wie gesagt, bei den meisten Systemen sind die Treiber an Bord. Man kann Btrfs Disks ganz normal mit “mount” einbinden. Was nicht bei allen Systemen an Bord ist, sind die Userspace-Tools (Im Wesentlichen das Utility-Programm btrfs). Wenn nötig, installiert man sie mit sudo pacman -S btrfs-progs bzw. sudo apt-get install btrfs-tools.

Konzept

Wie ZFS ist auch Btrfs gleichzeitig ein Festplattenverwaltungstool und ein Dateisystem. Man kann ein Btrfs Dateisystem entweder auf einer Partition eines anderweitig partitionierten Datenträgers installieren, oder auf einem ganzen Datenträger. So oder so kann man das Filesystem später dynamisch erweitern. Eebnfalls ganz ähnlich wie bei ZFS kann man mehrere Datenträger zu Raid0, Raid1 oder Raid5 -Verbänden zusammenschliessen. Bei Btrfs kann man allerdings for Daten und Metadaten getrennt angeben, in welcher Form man sie verwalten wird.

In medias res

Ein neues Btrfs-Dateisystem erstellt man zum Beispiel so:

makefs.btrfs -m raid1 -d raid1 -f /dev/sdc /dev/sdd

Hier haben wir jetzt also zwei ganze Platten, sdc und sdd (welche gleich gross sein sollten) verknpftm, um darauf ein btrfs-Dateisystem zu erstellen, bei welchem sowohl die Daten (-d), als auch die Metadaten (-m) als Raid1 organisiert sind, also auf beide Platten gespiegelt werden. Wenn wir nur einzelne Partitionen verwenden wollten, hätten wir auch diese angeben können:

makefs.btrfs -m raid1 -d raid0 -f /dev/sdc1 /dev/sde8

Das -f bewirkt, dass Btrfs die Aufgabe auch dann erledigt, wenn bereits ein Dateisystem oder eine Partitionierung auf dem Ziel vorhanden ist. Aber damit tut es das wirklich sofort, also bitte dreimal kontrollieren, ob wirklich die richtigen Medien formatiert werden…

Snapshots

Die sind auf den ersten Blick ein wenig umständlicher, als bei zfs. Sie basieren auf “Subvolumes”, und werden darum in den üblichen Btrfs-Dokumentationen erst nach den Subvolumes erwähnt, und wenn man vom schwer verständlichen Subvolume-Konzept schon so ermüdet ist, dass man gar nicht mehr folgen kann, wenn die Rede dann auf Snapshots kommt.

Dabei ist es eigentlich nicht so arg schwierig: Subvolumes sind von aussen gesehen einfach Verzeichnisse innerhalb des Btrfs-Dateisystems. Das Besondere daran ist, dass man sie unabhängig mounten kann. Man erstellt ein Subvolume mit btrfs subvolume create /path/to/btrfs/subvolume oder, einfacher: btrfs sub create ... (Man kann alle Btrfs-Befehle so weit abkürzen, dass sie noch eindeutig sind). Und wenn man so weit ist, kann man nach Herzenslust Snapshots erstellen. Ein Snapshot ist ein exaktes Abbild eines Subvolumes, dessen Erstellung nur Sekunden dauert, egal wieviele Daten darin sind.

Daher an dieser Stelle ein etwas längeres Beispiel eines Backup-Konzeptes:

sudo mount /dev/sdc /mnt/raid
sudo btrf sub create /mnt/raid/backups
sudo btrfs sub create /mnt/raid/backups/full
sudo mkdir /mnt/raid/backups/snapshots

Dann kann man ein Backup-Skript wie folgt erstellen:

today=`date '+%Y-%m-%d'`
dest=/mnt/raid/backups
rsync -aAXHSv --exclude-from=whatToExclude.txt / ${dest}/full
btrfs sub snap -r ${dest}/full ${dest}/snapshots/full-$today

Nach jedem Lauf dieses Skripts hat man ein aktuelles Backup in /mnt/raid/backups/full, und alle früheren Backups in /mnt/raid/backups/snapshots

Ähnlich wie bei ZFS kann man Snapshots auch auf weitere Laufwerke oder an andere Btrfs-Instanzen senden. Der Befehl dazu lautet btrfs send -f destfile /path/to/snapshot. Wenn man nur eine Differenz zu einem früheren Snapshot senden möchte, macht man: btrfs send -p /path/to/older/snapshot -f destfile /path/to/new/snapshot

29.7.2016 - Arch Linux

Arch Linux hat ja den Ruf, ein Linux für eher hartgesottene Freaks zu sein.

Richtig ist, dass man bei Arch Linux mehr selber machen muss, als bei anderen Distributionen. Allerdings gibt es Arch-Varianten, die eher anfängerfreundlich gestaltet sind, z.B. Manjaro.

Ein Vorteil von Arch ist, dass es wirklich nur das enthält, was man braucht, und dass man nach der Installation schon ziemlich viel über den Aufbau “seines” Linux Systems weiss. Denn Arch hat keinen simplen Installer, wie etwa Ubuntu. Stattdessen gibt es bei Arch eine ausführliche Anleitung, wie man das System aus einem laufenden Linux heraus von null an zusammenbaut.

Ein weiterer Vorteil ist, dass ein Arch System leicht aktuell gehalten werden kann: Einerseits funktioniert es nach dem “Rolling Release”-Prinzip, es gibt also keine Release-Stufen, sondern einen kontinuierlichen Update-Prozess. Und andererseits besteht das Package-System von Arch aus einer automatisierten “Bedienungsanleitung”, wie die Software direkt aus den Quellen zusammengeabut und installiert wird. Man braucht also nicht darauf zu warten, dass der Distributions-Maintainer Updates bereitstellt.

Doch vor den Erfolg haben die Götter den Schweiss gestellt…

Installation

Die Grundinstallation ist auf der Archlinux Seite sehr gut beschrieben.

Wie es dann weiter gehen kann, kann man ebenfalls bei Arch Linux nachlesen. Auch hier wird man vieles, was andere Distributionen einem vorfertigen, noch von Hand erledigen müssen (Zum Beispiel, einen User und Gruppe anlegen). Im Folgenden arbeiten wir immer als “normaler” User, nicht als root.

Aber zunächst muss auch noch das Package-System anegkegt werden. Arch hat einen Package Manager namens “pacman”, der die Aufgaben erledigt, die man in debian-Derivaten gerne aptitude oder apt-get anvertraut. Das Standardkommando ist synchronize: pacman -S <paketname>. Um das ganze System auf den neuesten Stand zu bringen. schreibt man pacman -Syu.

Auch eine Klickibunti-Oberfläche muss man selber anlegen, denn der Arch Installationsprozess führt zunächst nur mal nur zu einem reinen Textsystem. Das hat wiederum den Vorteil (oder ist es ein Nachteil?) der grossen Auswahl:

Praktisch alle Fenstersysteme gibt es auch für Arch. Sie können also frei entscheiden, ob Sie etwa Gnome oder KDE oder Lxde oder Mate oder was auch immer haben möchten. Aber Sie müssen es selber zusammenstöpseln. Daher im Folgenden eine sehr subjektive Zusammenstellung.

Fenstersystem

Zunächst mal gibt es da viele Begriffe, die zusammenspielen:

  • Display Driver: Steuert direkt die Grafikkarte an. Im Prinzip ist es heutzutage problemlos ohne Verrenkungen möglich, den Bildschirm mit (fast) jeder Grafikkarte zu einer Anzeige zu bewegen. Aber je nachdem, wie hohe Ansprüche man an die 2D/3D Grafikleistung stellt, wird man mehr oder weniger Aufwand treiben müssen, um einen möglichst genau zur eigenen Grafikkarte passenden Treiber zu finden. Man kann sich aber ohne Weiteres fürs Erste mal auf die automatisch installierten Treiber verlassen und später weiter schauen.

  • Display Server / X-Server: Zuständig für die Umwandlung von Programmbefehlen in Bildschirmpunkte. Also die niedrigste Ebene der Anzeigensteuerung. Ausserdem zuständig füs Umleiten der Anzeige an ein anderes Terminal, wenn gewünscht.

  • Display Manager: Zuständig für den Start des Fenstersystems. Erlaubt meist auch beim Login die Auswahl aus mehreren installierten Fenstersystemen.

  • Window Manager: Fenstersystem. Über dem Display Server stehende Ebene der Bildschirmansteuerung. Der Window-Manager baut Fenster und Fensterelemente auf und stellt die coolen Ein/Ausblend/Transparenzeffekte zur Verfügung.

  • Desktop Environment: Setzt auf dem Window Manager auf und sorgt für einheitliches Design und Verhalten (Themes).

In einem Linux-System braucht man nicht all diese Elemente. Wenn man einen Leistungsschwachen Computer hat, installiert man vielleicht nur den X-Server. Programmen wie xterm etx. genügt das. Oder man eght einme Stufe weiter, und installiert noch einen Window-Manager. Dann laufen die Programme in hübschen Fenstern, aber man hat noch keinen Desktop, kein Menü und keine Taskleisten. (Man kann allerdings diese Dinge durchaus einzeln nachinstallieren).

Meistens wird man heutzutage ein kompklettes Desktopsystem installieren. Dann sieht die Oberfläche aus wie aus einem Guss und funktioniert auch so, wie wir es nunmal gewöhnt sind.

Mir gefällt der Cinnamon-Desktop von Linux Mint. Natürlich kann man den auch in arch installieren. Damit der Computer gleich in den Desktop startet, füge ich gleich noch den Display Manager gdm hinzu.

Also: sudo pacman -S gdm cinnamon

Achtung: Das System bootet trotzdem zunächst in den Textmodus. Um den Desktop zu starten, muss man sudo systemctl start gdm eingeben, oder, wenn man beim Systemstart auotmatisch in den Grafikmodus kommen will, sudo systemctl enable gdm. So oder so muss man beim ersten Start, bevor man das Login-Passwort eingibt, auf das Zahnrad unter dem Namen klicken, und das gewünschte Fenstersystem, auswählen. Sonst landet man in einer eher langweiligen Stanmdardoberfläche.

Drucker

Der Anschluss von Hardware ist unter Linux ja immer ein besonderer Quell der Freude. Natürlich gibt es immer alles, aber meistens in -zig Varianten und Versionen und ohne komplette Bedienungsanleitung. Der Prozess der Druckerinstallation ist immerhin dank cups (common unix print system) ein Stück weit automatisiert.

sudo pacman -S cups gutenprint foomatic-db-gutenprint

installiert eine Reihe von Druckertreibern. Die Konfiguration erledigt man dann am besten entweder mit dem Drucker-Systemsteuerungs-Applet von Cinnamon, oder über die Webseite des CUPS-Servers: http://localhost:631

Nach einigem Hin und Her gelang es mir damit, meine Drucker (Brother HL-4570CDW und Brother MFC 8510 übers Netz, Canon MG 5550 am USB-Port) anzusteuern. Bei manchen Druckern, bei mir etwa dem Dymo LabelWriter 400/450 kann man Linux Treiber direkt beim Hersteller downloaden. Die funktionieren gerade bei Spezialdruckern im allgemeinen besser, als die Standard-Treiber von Gutenprint.

Scanner

Die Scannerinstallation ist theoretisch sehr einfach geworden:

sudo pacman -S sane simple-scan

Damit wird das Backend (sane) und ein einfaches grafisches Frontend installiert. Sehr viele Scanner werden von sane (scanner access now easy) problemlos automatisch erkannt und eingebunden. Ein probeweises scanimage -L zeigt, welche Scanner sane gefunden hat.

Meiner war leider nicht dabei. Ein nwenig Recherche im Internet zeigte, dass die Dinge für den Fujitsu ScanSnap s1300i leider etwas komplizierter sind. Fujitsu vertreibt Treiber für Mac und Windows, aber nicht für Linux. Und die Schnittstellen sind offenbar nicht dokumentiert, so dass es auch keine OpenSource Treiber gibt.

Also ging ich nach der Recherche am Ende folgendermassen vor:

  • Die Windows-Treiber auf einem Windows-Computer installieren
  • Den Computer mit der Dateisuche nach *.nal Dateien durchforsten. Glücklicherweise gibt es nicht sehr viele Dateien mitn dieser Endung. Zwei davon waren dieselben, die auch in /etc/sane.d/epjitsu.conf aufgeführt sind.
  • Diese beiden Dateien auf den Linux-Computer nach /usr/share/sane/epjitsu kopieren.

Et voilà: Jetzt findet scanimage -L den Scanner, und simple-scan kann damit auch Dokumente einziehen, problemlos auch doppelseitig und mehrseitig.

Office Zubehör

yaourt -S openoffice4
sudo pacman -S evince gedit    

DICOM Viewer und PACS client

yaourt -S ginkgo-cadx

Hier ist die Auswahl nicht besonders gross. Es gibt ein paar Projekte, die zum Beispiel bei Ubuntu gelistet sind. Wenn man konkret nachschaut, gibt es darunter aber Projekte, die aufgegeben zu sein scheinen (z.B. Aeskulap), oder die nur sehr rudimentäre Funktionen bereitstellen. Kurz: Ein ernsthafter Konkurrent für Osirix (oder seine freie 64-Bit-Variante OsiriLXIV) ist nicht dabei. Aber leider stehen die Osirix Varianten nicht zur Debatte, da nur für Mac.

Ziemlich gut finde ich Ginkgo-CADx. Die Bedienung ist etwas gewöhnungsbedürftig, aber wenn man es erst mal raus hat, kann man damit eigentlich fast alles machen, was man auch mit OsiriX kann. Nur zum CD Brennen muss man externe Tools bemühen, und auf 3D-Animationen muss man verzichten. Dafür hat man ein paar Dinge, die man mit OsiriX nicht kann, zum Beispiel Bilder aller Art “dicomisieren”.

#### Diverses

  • Viele Mac-User sind an Quicksilver oder Alfred gewöhnt und vermissen das auf Linux. Ein Klon mit praktisch identischer Funktionalität ist Kupfer, welches in arch mit yaourt -S kupfer installiert werden kann.

  • Ein einfacher CD-Brenner ist Brasero -> sudo pacman -S brasero.
  • Computer durchsuchen: Catfish sudo pacman -S catfish.

15.7.2016 - Eclipse .classpath

In Java hat der classpath ja eine klar definierte Bedeutung: Er enthält alle Orte, an denen der Java Compiler und die JRE nach Klassen suchen, die das aufgerufene Programm beim Ablauf benötigt. Man übergibt den classpath beispielsweise mit java -cp /some/where:/some/other/place my_app.

In Eclipse hat jedes Projekt eine versteckte Datei .classpath, die ähnliche, aber nicht genau dieselben Aufgaben hat. In .classpath ist das Projektlayout festgelegt. Eclipse folt nämlich einer etwas anderen Philosophie, als etwa Maven:

In Maven gilt strikt “convention over configuration”. Das bedeutet, Maven erwartet, alle Teile eines Projekts an einem ganz bestimmten relativen Ort vorzufinden. Beispielsweise wird der Quellcode immer in src/main/java erwartet. Man kann das durchaus ändern, aber dazu muss man ausdrückliche Anweisungen in der pom.xml schreiben.

In Eclipse ist das umgekehrt. Eclipse erwartet Quellcode keineswegs ausdrücklich immer in ‘src’, obwohl das in Projekten standardmässig so ist. Eclipse “weiss” erst, wo die Quellen sind, wenn die Orte in .classpath festgelegt sind.

Beide Techniken haben ihre Vor- und Nachteile. Aber man sollte die Unterschiede kennen, wenn man Programme mit beiden Systemen entwickelt.

Wenn man beispielsweise .classpath in .gitignore aufführt, dann wird das Projekt nach dem Import in eine andere Eclipse-Instanc zunächst nicht kompilieren. Der classpath muss zuerst korrigiert werden. Wenn man .classpath aber ins Repository aufnimmt, dann kommt es häufig zu “Classpath-Problems”, weil manchmal implizite Systemabhängigkeiten zum Beispiel bei Libraries in .classpath aufgeführt sind.

Derartige Probleme hat man mit der Maven-Methode nicht. Dafür kann man dort nicht so einfach, mehrere Quellcode-Hierarchien in einem Projekt integrieren.