Frustrert Java-utvikler om Spring: - Føles som et fengsel!

Johannes Brodwall om hvordan han bygger applikasjoner uten Spring eller andre rammeverk.

Johannes Brodwall har lite til overs for Spring, da han mener rammeverket løser problemer som ikke er reelle i Java lenger. 📸: Privat / Ole Petter Baugerød Stokke
Johannes Brodwall har lite til overs for Spring, da han mener rammeverket løser problemer som ikke er reelle i Java lenger. 📸: Privat / Ole Petter Baugerød Stokke Vis mer

“Hvorfor bruker du egentlig ikke Spring-rammeverket slik som oss andre?”

Det er få ting som får meg mer frustrert enn denne type spørsmål. I motsetning til det en del som bruker det ser ut til å tro, så er det populære Spring-rammeverket ikke en del av standardplattformen i Java. Og i motsetning til det mange ser ut til å vite, kan det i innføre vel så mange problemer som det løser.

Som en liten julegave har jeg lyst til å dele med mine Java-frender hvordan jeg liker å bygge “enterprise”-applikasjoner, uten Spring eller tilsvarende rammeverk.

Men først bør vi kanskje ta en titt på Spring og hva som er problemet med det.

Historien til Spring

Som et rammeverk som er eldre enn de yngste Java-programmererne, bygger rammeverket på en del historie. En del er vann som heldigvis har rent videre.

Men det bygger også på en fundamental avgjørelse som ble tatt for lenge siden, allerede da skaperne av Java først skulle lage rammene for web-applikasjoner i Java med HttpServlet-rammeverket:

Da man skrev en servlet på 90-tallet (!) kunne man ikke selv instansiere klassen sin. Det var det applikasjonsserven som gjorde. Det betyr at man trengte “en løsning” på hvordan å sette opp konfigurasjonen. Og løsningene som kom med servlet-rammeverket var dårlig.

Det vi ønsker oss er jo noe sånt:

MyConfiguration configuration = new MyConfiguration();
configuration.readConfiguration(new File("myapp.properties"));

MyApplicationServlet servlet = new MyApplicationServlet();
servlet.setDataSource(configuration.getDataSource());

Men i hine hårde dager kunne man verken gjøre noen antagelser om filsystemet, man kunne ikke instansiere servlets selv og man hadde ingen steder å legge denne type kode.

Her kom Spring og reddet oss på et vis.

På 2000-tallet kunne vi skrive kode som:

<beans>
   <bean class="my.application.MyApplicationController">
       <property name="dataSource" ref="dataSource" />
   </bean>
  
   <bean class="DataSource" id="dataSource">
       <!-- -->
   </bean>
  
</beans>

Dette ble fort tungvint. Så Spring innførte mer automatikk med annotations som @AutoWire, @Bean og @AutoConfiguration. Nå skriver man bare en klasse, annoterer den og så bare skjer ting.

Ting bare skjer

Men her kommer vi til den viktigste kritikken av Spring-rammeverket: Ting bare skjer. Det kan være kjekt de første ukene med et nytt prosjekt, spesielt dersom man holder seg innafor de mest vanlige rammene. Men etterhvert som prosjektet vokser seg større så blir det vanskeligere og vanskeligere å resonnere rundt.

Ting skjer, men man forstår ikke hvorfor.

«Virkeligheten er nemlig at den ene begrensningen som startet hele Spring-eventyret er ikke lenger en begrensning.»

Når Java-utviklere sitter rundt bålet og snakker om Spring kommer så mang en historie om en episode der de brukte dagevis på å finne ut av en eller annen mystisk oppførsel i et Spring-prosjekt. Siden koden scannes dynamisk og objekter skrues sammen basert på navn og interface-tolkning gir ikke utviklingsverktøyene på langt nær så god støtte til å navigere og utforske koden som vanlig Java-kode. Og tilsynelatende trivielle refactorings kan uante følger.

Det mest alvorlige er at denne formen for scanning-basert konfigurasjon gjør Spring-applikasjoner mer utsatt for supply-side angrep: En ny versjon av en eksisterende avhengighet kan lure inn en ny controller som eksponerer en bakdør. Appen som tar den i bruk får ikke nødvendigvis noen varselsbjeller.

Det er derfor, når jeg får spørsmålet “hva slags rammeverk bruker du i stedet for Spring”, så hører jeg “hva slags fengselskule bruker du i stedet for den grå fengselskula.” Virkeligheten er nemlig at den ene begrensningen som startet hele Spring-eventyret er ikke lenger en begrensning. Små endringer i hvordan appservere som Jetty (og Tomcat) fungerer, samt nye måter å deploye på har endret forutsetningen.

Slik klarer du deg uten Spring

Da har tiden kommet for å dele litt om hvordan jeg setter opp mine applikasjoner. Skal vi starte ytterst?

Jeg deployer nå på Kubernetes. (Jeg brukte lenge en nesten identisk innfallsvinkel på virtual machines, så man klarer seg også uten). Slik ser min Dockerfile ut:

FROM openjdk:11.0-jre

RUN mkdir -p /app/lib
COPY target/dependency/* /app/lib/
COPY target/classes /app/classes

WORKDIR /app
CMD ["java", "-classpath", "/app/classes:/app/lib/*", "com.soprasteria.johannes.MyServer"]

Vi kopier inn dependencies og egen kode og ved oppstart kjører vi en main-klasse. Et lite knep er alt jeg trenger for å få med meg alle dependencies fra Maven-prosjektet mitt (tilsvarende kan gjøres med Gradle osv):

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-dependency-plugin</artifactId>
   <version>2.4</version>
   <executions>
       <execution>
           <phase>package</phase>
           <goals>
               <goal>copy-dependencies</goal>
           </goals>
       </execution>
   </executions>
</plugin>

Dette kopierer alle dependency jar-end inn i target/dependency, slik at de kan kopieres inn i Docker. Med disse små grepene har jeg min kode og all kode jeg er avhengig av inni en Docker-container. MyServer er heller ikke så omfattende:

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.webapp.WebAppContext;

import javax.sql.DataSource;

public class MyServer {
   private final Server server = new Server();
   private final ServerConnector connector = new ServerConnector(server);
   private final MyApplication myApplication = new MyApplication();

   public static void main(String[] args) throws Exception {
       MyServer server = new MyServer();
       server.setHttpPort(Integer.parseInt(System.getenv("HTTP_PORT")));
       server.setDataSource(createDataSource(
               System.getenv("DATASOURCE_URL"),
               System.getenv("DATASOURCE_USERNAME"),
               System.getenv("DATASOURCE_PASSWORD")
       ));
       server.start();
   }

   private void setDataSource(DataSource dataSource) {
       myApplication.setDataSource(dataSource);
   }

   private void setHttpPort(int httpPort) throws Exception {
       connector.setPort(httpPort);
       connector.start();
   }

   private void start() throws Exception {
       WebAppContext handler = new WebAppContext("/webapp", "/");
       handler.addEventListener(myApplication);
       server.setHandler(handler);
       server.start();
   }

   private static DataSource createDataSource(String url, String user, String password) {
       // Overlatt til leserens fantasi
   }
}

Koden over benytter Jetty for å sette opp en server og en port og så plasserer jeg oppsettet av applikasjonslogikken i MyApplication (vi kommer tilbake til denne snart). Koden leser HTML-filer fra classpath:/webapp og server http-requester på “/” (ROOT).

Med mer konfigurasjon, logging og flere behov kan dette vokse seg litt større så jeg må naturligvis passe på å strukturere koden min godt. Jeg har blant annet noen knep for å få til mer smidig lastning av HTML-filer når jeg endrer dem, men det tar litt for mye plass her. Det siste elementet er klassen MyApplication. Denne bruker en ContextListener - en del av servlet-standarden. Så herfra har jeg ikke lenger bundet meg til Jetty:

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;

public class MyApplication implements ServletContextListener {

   private final MyServlet myServlet = new MyServlet();

   @Override
   public void contextInitialized(ServletContextEvent sce) {
       ServletContext context = sce.getServletContext();
       context.addServlet("myServlet", myServlet).addMapping("/myServlet");
   }

   public void setDataSource(DataSource dataSource) {
       myServlet.setDataSource(dataSource);
   }
}

Applikasjonen legger til en enkelt servlet på “/myServlet”. Dersom jeg hadde hatt flere servlets ville jeg puttet disse inn tilsvarende.

For å starte applikasjonen trenger jeg bare å starte MyServer som en main-klasse og passe på å ha med HTTP_PORT som en environment variabel. Siden jeg kan kjøre denne main-klassen fra min IDE trenger jeg ikke gjøre noe spesielt for debugging, enhetstesting eller andre utvikleroppgaver.

Du lurer kanskje på hva som ligger i MyServlet?

Det kan være opp til deg. Det kan være en gammaldags HttpServlet der du implementerer doGet og doPost eller det kan være et rammeverk som router requester på et fancy vis. Jeg har … kremt… laget mitt eget rammeverk for http-håndtering 😊. I motsetning til antagelsene i gamle dager er det kun én instans av MyServlet i applikasjonen, så du kan selv “injecte” de objektene du vil i servleten, helt enkelt ved å kalle settere. (Men ikke bruk member fields til å lagre verdier fra én request!)

Dependency injection er et pattern, ikke et rammeverk

Å lage en god Java-webapplikasjon krever fortsatt arbeid, struktur og kanskje et http-rammeverk. Men jeg håper koden min viser at behovet for rammeverk rundt håndteringen av “livssyklusen” til programmet ditt og konfigurasjon, nei det trenger man faktisk ikke. Helt vanlig mekanismer i språket som member fields, constructors og settere er helt tilstrekkelig for å sette opp strukturen på en server.

Applikasjonene mine vokser seg naturligvis store etterhvert og krever organisering. Men et løfte går jeg ikke bort fra: Når du ser server-klassen min kan du control-click navigere deg direkte for å finne applications, servlets, controllers, connectors og alt annet som inngår i applikasjonens struktur. Jeg vil at jeg skal kunne ressonere om systemets sammensetninger ved å navigere koden, ikke ved å lese dokumentasjonen om bean-autowire regler.

«Når jeg bruker Spring føles det som om jeg er i et fengsel og får noen få lufteturer om dagen med fengselskula hengende rundt beinet.»

En vanlig definisjon på et rammeverk er at et rammeverk er bibliotek der det er rammeverket som instansierer dine klasser i stedet for at du instansierer det. Der det er rammeverket som bestemmer livsyklusen: Det er rammeverket som bestemmer når din kode skal leve og når den skal dø.

En konsekvens av rammeverket er at du også fort kan miste kontrollen over alle bibliotekene rammeverket tar med seg inn. I disse tider hvor internett fortsatt står i brann over #log4shell kan det være en kontroll du vil ta tilbake.

Når jeg bruker Spring føles det som om jeg er i et fengsel og får noen få lufteturer om dagen med fengselskula hengende rundt beinet. Et vanlig argument for Spring er at det er lurt å bruke den fengselskula som flest mulig er vant med. Personlig har jeg tatt av meg lenka og sluttet å se etter en ny fengselskule.