(toiminnot)

hwechtla-tl: The daily Java WTF

Kierre.png

Mikä on WikiWiki?
nettipäiväkirja
koko wiki (etsi)
viime muutokset


ke 1.3.2017

Javassahan ei ole mitään tapaa eikä käytäntöä sille, miten valmis, käännetty softa (joka yleensä levitetään .jar- tai .war-tiedostona) löytää käyttämänsä kirjastot. Siksi ne kirjastot yleensä levitetään .jar-tiedoston mukana. Tähän on yleistynyt käytäntö, että build-työkalut osaavat paketoida kirjasto-.jar:t softa-.jar:n sisään, niinsanotuksi "überjar":ksi.

Yhdistetäänpä tämä siihen, että on tyypillistä käyttää joka softaan kaiken maailman frameworkeja ja kirjastoja, jotka puolestaan vetävät lisää kirjastoja sisään. Seuraus on, että suhteellisen yksinkertainen API-toteutus, joka käyttää Spring Bootia, Google Guavaa, Lombokia ja JAXBia, vie käännettynä .jarrina levyltä 38 megatavua. Mutta ei se ole varsinainen ongelma, vaan se, että sitten javalla ajettuna tämä .jar vie muistia 3 gigatavua josta muistissa 300 megatavua. Niin että jos luulette, ettei modernia pöytäkonetta saa polvilleen muutamalla mikroservicellä, niin kyllä saa. Kunhan valitsee teknologiaksi Javan.

ma 27.2.2017

Spring-WS ei oletuksena lokita poikkeuksia, jotka tapahtuvat @Endpoint-kontrollereissa. Spring-bootin @RestController:it sen sijaan lokittavat.

Spring-WS:n virheenkäsittely on muutenkin ihan tolkutonta. Jos SOAP-viestissä on virhe, virhevastaus on JSONia.

ti 24.1.2017

Spring Boot suhtautuu oletusasetuksilla katkenneisiin tietokantayhteyksiin näin: niistä tulee poikkeus, yhteys merkitään suljetuksi ja palautetaan connection pooliin. Käytännössä tämä tarkoittaa, että kun tietokantayhteys menee poikki, sitä ei palauteta ja lisäksi vaikka tietokanta tulisikin takaisin, poolissa on yhteysolioita joita Spring yrittää käyttää mutta ei onnistu koska niiden yhteys on jo merkitty suljetuksi. ("There is no such database connection.")

Tässä toimintatavassa ei ole mitään järkeä missään käyttöskenaariossa.

On olemassa asetus spring.datasource.test-on-borrow=true, joka katsoo aina ennen tietokantayhteyden käyttöönottoa, onko se vielä auki. Mutta mitä jos vaikka Spring käsittelisi SQLException:it ja poistaisi poolista yhteyden, kun se menee katki? Eikö olisi suoraviivaisempaa?

ma 23.1.2017

Käsitteleeköhän joku Clojurella oikeasti XML:a? Kokeilin toteuttaa SOAP-palvelua Clojurella, ja totesin, ettei ole oikein mitään Clojure-kirjastoa, joka osaisi käsitellä XML-puita, joissa elementtien ja/tai attribuuttien nimet ovat nimiavaruudellisia. Ei ole edes yhtenäistä käsitystä siitä, millaisina tietorakenteina esitetään nimiavaruudellinen XML. Tosi outoa. Ks. esim. nämä: http://grokbase.com/t/gg/clojure/127q8q3mxk/xml-parsing-with-namespace-prefixes http://stackoverflow.com/questions/18898823/clojure-namespace-aware-xml-parser-zipper

ti 17.1.2017

Spring Bootissa ei ole mitään varsinaista tukea sellaisille testifixtureille, jotka takuuvarmasti tapahtuvat ennen mitään testejä. Jos kaikki testini käyttävät samaa "sovelluskontekstia", voin laittaa kyseiseen kontekstiin beanin, joka huolehtii sopivasta alustuksesta. Mutta jos tarvitsen testeihini monta eri kontekstia, mihin voin laittaa alustukset, jotka ajetaan tasan kerran ja aina ennen kaikkia testejä?

SQL-kannan alustamiseen tällä tavoin on erillinen tuki, mutta sekin hajoaa, jos käyttää monta kontekstia.

ma 16.1.2017

En varmaan ole koskaan vielä valittanut, että Java on verboosi? Streamit korjaavat tätä, mutta niiden lukeminen aiheuttaa sivuvaikutuksena, ettei niitä voi lukea uudestaan. Niinpä, jos haluan kirjoittaa kunnolla funktionaalista koodia, jokainen yksinkertaisinkin map näyttää joltain tämmöiseltä:

final List<String> names = customers.stream().map(c -> c.name).collect(Collectors.toList());

Ainoa tosissaan vaihteleva asia tässä on tuo "c -> c.name", kaikki muu on vain toistoa joka kerta, kun tehdään jokin mappaus.

pe 30.12.2016

Hibernate käyttää oletuksena sekvenssejä siten, että kun tauluun luodaan uusia rivejä, niiden ID:ksi ei tule se luku, joka sekvenssistä saadaan. Niinpä Hibernatella luodut rivit hajottavat kaikki muut ohjelmat, jotka yrittävät luoda tauluun rivejä saman sekvenssin avulla.

ke 14.12.2016

Springissä pystyy tekemään automaagisesti CRUD-ORM:n (ks. object relational mapper) JPA:n @Entity-luokkien perusteella. Se on niin automaaginen, että Intellij IDEA:n DI-tuki ei tiedä, että näiden CRUD-repositoryjen beanit itse asiassa on olemassa, vaan valittaa, ettei sellaista beania löydy (jos sen yrittää esim. @Autowired-annotaatiolla laittaa johonkin). Softa toki suorittuu kyllä.

Jostain käsittämättömästä syystä näillä @Entity:illa pitää aina olla tyhjä, argumentiton protected-konstruktori. Ööh?

Sillä ei ehkä olisi muuten väliä, mutta tästä syystä esimerkiksi Lombokin saaminen toimimaan näiden @Entity-luokkien kanssa vaatii kikkailua.

pe 14.10.2016

Spring-WS on todella huonosti dokumentoitu sen suhteen, miten napataan virheellisiin SOAP-viesteihin liittyvät poikkeukset (jos SOAP-osa on virheellinen). Oletustoimintatapa on ala-arvoinen: HTTP 400 ja tyhjä body.

su 9.10.2016

Clojure on yleensä aika ortogonaalinen, mutta:

user=> (meta ^{:a 1 :b 2} [1 2 3])
{:b 2, :a 1}
user=> (def a [1 2 3])
#'user/a
user=> (meta ^{:a 1 :b 2} a)
nil

Ilmeisesti tuo reader macro ^{ on jotenkin viritetty vain tietynlaisille lausekkeille. with-meta toimii kaikelle.

to 6.10.2016

Tähän ihmiset ovat ehkä niin tottuneita, etteivät osaa enää sitä kyseenalaistaa, mutta Javassa on _tolkuton_ määrä luokkia, joiden ainoa pointti on jollain tavalla sisältää tekstiä. Esimerkiksi String, StringBuffer, char[], StringReader, StringWriter, Reader, BufferedReader, InputStream, BufferedInputStream, ... joillekin näistä on toki syitäkin, mutta tilanne on tosi monimutkainen useimpiin kieliin verrattuna, ja jotkin muunnokset näiden välillä (esim. String->InputStream) ovat sen verran epämääräisiä, että niille on erikseen apufunktiot Apachen IOUtils-luokassa. Mutta ei perus-Javassa :(

Monet kirjastokutsut on ylikuormitettu ottamaan vastaan "mitä tahansa, mistä voi lukea syötettä", siis useampia näistä tekstiä sisältävistä luokista. Sitten vaihtelee randomisti, tulkitaanko String-syöte itse syötteeksi vai syötteen sisältävän tiedoston nimeksi (tai URLiksi).

ma 19.9.2016

Java 8:ssa on lisätty java.time-package, joka tuo kieleen viimeinkin edes jotenkin käyttökelpoiset luokat aikojen käsittelyyn. Aiemmat vaihtoehdot (java.util.Date, java.util.Calendar jne) ovat ihan käsittämättömän hähmäisiä. Miten ihmeessä tähän on voinut kestää näin kauan? Tämä alkaa vaikuttaa minusta tärkeämmältä syyltä päivittää Java 8:aan kuin streamit.

Tietysti on voinut käyttää joda-timea. Mutta siis: Java-maailmassa seikkailu on vähän tällaista: jokaista hyvää kirjastoa kohden on kolme huonoa, ja on vaikea selvittää nopeasti, mikä niistä on oikeasti hyvä, koska on niin paljon vanhentunutta tai kokemattomien ihmisten kirjoittamaa dokumentaatiota.

pe 16.9.2016

java.util.Date:n ajalla ei ole aikavyöhykettä, mutta sen .toString()- ja .getHours()-metodit salaa käyttävät localesta saatua aikavyöhykettä, jolloin vaikuttaa siltä, että Date-oliot ovat paikallisessa aikavyöhykkeessä (ellei softaa ajeta jossain oudossa localessa).

Tämä on hämäävää, mutta lisäksi se tarkoittaa, että koodi alkaa oikeasti toimia arvaamattomasti eri tavalla, jos sen localea vaihtaa.

to 15.9.2016

Jos Jenkinsin käynnistää suomenkielisessä ympäristössä ilman konffista, sen pääsivulla lukee: "luoda uusia työpaikkoja , jotta pääset alkuun."

ti 13.9.2016

Camel-frameworkissa voi tehdä "reittejä", jotka siirtävät viestejä eri systeemien välillä. Jostain syystä reitti, joka vie HTTP:llä rest()-direktiivillä määritetystä osoitteesta to("http4://...")-direktiivillä määritettyyn osoitteeseen, ei suostu toimimaan, ellei matkan varrella käsittely heitä roskiin alkuperäistä HTTP-osoitetta. Eli pitää sanoa removeHeaders(HTTP_URI). Muuten Camel valittaa, että http4://-osoite on "kelpaamaton". Ja antaa vihjeen lisätä siihen bridgeEndpoint-valitsimen, joka on kuitenkin se vaivalloisempi tapa.

pe 9.9.2016

Jos Camelissa muuntaa viestin formaatista toiseen, mutta unohtaa poistaa CONTENT_TYPE-headerin, Camel yrittää "automaattisesti" muuntaa viestin takaisin siihen formaattiin, josta se muunnettiin pois. Voisi kuvitella, että marshal() ja unmarshal() muuttaisivat myös headereita siten, että CONTENT_TYPE on oikein, mutta eihän nyt toki.

Kun sitten Camel tekee sen automaattimuunnoksensa, on tietysti ihan saatanan vaikea arvata, missä se vika on, koska ei ole mitään steppiä, missä voisi nähdä, mitä tapahtuu. Automaattimuunnos tehdään vasta siinä vaiheessa, kun viesti on lähtemässä ulos Camelista.

pe 26.8.2016

Springissä on dependency injection -annotaatio "@Bean", jolla voi kertoa, että tietyn luokan singleton on muihin olioihin injektoitava dependenssi tai tietyn (luokka)metodin tulos eli palautusarvo on sellainen. Sillä voi siis saada aikaan, että tietty luokka instantioidaan olioksi tai tiettyä metodia kutsutaan. Tämä toimii jopa, vaikka luotua beania ei sitten käytettäisi missään. Toisin sanoen, sillä voi luoda metodeita, joita Spring maagisesti kutsuu luodessaan sovelluskontekstia.

Jos näillä metodeilla on sivuvaikutuksia, on aika jänskää katsoa, löytääkö joku koodin ylläpitäjä, mistä helvetistä ne sivuvaikutukset tulevat ja miksi.

ke 24.8.2016

Gradle + Spring: molemmilla on juttuja, jotka ovat ihan kuin ympäristömuuttujia mutta eivät ole ympäristömuuttujia, molemmat kutsuvat niitä system properties -nimellä, eivätkä ne ole sama asia. Ja jos haluaa välittää gradlelta springille näitä muuttujia, pitää sanoa build.gradlessa (sopivassa targetissa):

systemProperties = System.properties

Sehän oli selvä!

to 18.8.2016

Jos Spring bootilla merkitsee jonkin luokan @Transactional, niin jokaista sen metodia kutsuttaessa perustetaan transaktio, niin kuin kuuluukin. Mutta vain silloin, jos niitä kutsutaan Springin dependency injection -systeemin kautta (ainakin, jos ei käytetä code weavingia). Mitä se käytännössä tarkoittaa? Se tarkoittaa, että jos metodeja jotenkin onnistuu kutsumaan dependency injectionin ohi, niissä ei tapahdukaan transaktiokäsittelyä, mutta niiden kutsuminen silti onnistuu muilta osin.

Tämä tapahtuu esimerkiksi tilanteessa, jossa unohdat kirjoittaa metodin deklaraatioon "public" ja kutsut sitä saman packagen toisesta luokasta. Metodi "toimii", mutta transaktiota ei voidakaan perua, jos tapahtuu poikkeus (koska sitä ei ole).

Ei mitään varoituksia.

15.8.2016

Koska Javassa prosessit koostuvat usein keskenään kommunikoivista singleton-olioista (jotka on usein jaettu repositoryihin, serviceihin ja kontrollereihin), on tyypillistä testata koodia siten, että jokainen olio testataan erikseen eli kun sen metodeja kutsutaan testikoodista, taakse vaihdetaan "mock" eli juuri ja juuri oikeaa taustapalvelua muistuttava olio. Seuraus: kun koodin toimintaa vaihdetaan, ei tarvitse päivittää ainoastaan testejä vaan myös niiden käyttämät mockit.

Puhumattakaan siitä, ettei metodeista oikeastaan voi ihan tarkkaan tietää, toimivatko ne samalla tavalla kuin "todellisessa" ympäristössään.

12.8.2016

Myös näkymien luomiseen Jenkinsissä ainoa vaihtoehto on Groovy-skripti: https://issues.jenkins-ci.org/browse/JENKINS-8927

11.8.2016

Apache Camel on kirjasto, jolla "vaikeat" asiat kuten HTTP-kutsujen proxyamisen voi kuvata erityisellä DSL:llä, joka koostuu peräkkäisistä metodikutsuista tyyliin

teereitti("poksis")
  .kun(sataa).niin().ota(sateenvarjo).loppukun()
  .kun(paistaa).niin().ota(aurinkohattu).konfiguraatiolla(teelieri().iso());

Niinpä näistä periaatteessa ohjelmankaltaisista asioista tulee tietorakenteita, joita Camel sitten suorittaa sisäisellä "tulkillaan". Seuraus on se, että jos jokin menee pieleen, esim. backtracesta ei ole oikeastaan mitään hyötyä. On jopa vaikeaa päätellä, mistä kohdasta käsittelyn määrittelyä virheilmoitus aiheutuu: onko se kun(sataa) vai ota(sateenvarjo). Tai miten käsittely on edennyt.

10.8.2016

Tämänpäiväinen WTF on Java-softasta, ei kielestä tai sen kirjastoista itsestään. Nykyäänhän monenkin mielestä paras/kätevin/monipuolisin CI-palvelimen softa on Jenkins, joka on Javalla tehty. Se on siis softa, joka on suunniteltu ajamaan skriptejä säännöllisesti tai tietyssä tilanteessa (kuten jos versionhallinnassa jokin haara on päivittynyt). Kuten cron.

Jotkin asiat ovat Jenkinsillä _todella_ monimutkaisia. Jos esimerkiksi haluan asettaa ympäristömuuttujia jossain buildissa, voin tietysti käyttää vanhaa kunnon shelliä, mutta jos haluan käyttää jenkinsin omia systeemejä, pitää ensin asentaa envinject-plugin ja sitten lisätä build step, jossa niitä ympäristömuuttujia kirjoitetaan. Mikä vielä pahempaa, Jenkins on tarkoitettu automatisoimaan kehitysympäristöjen päivityksiä, mutta Jenkinsin _itsensä_ päivityksen automatisointi on salatiedettä. Jos haluan lisätä käyttäjän Jenkinsiin komentoriviltä:

  1. Pitää ladata jenkins-cli.jar jenkinsiltä esim. curlilla
  2. Pitää tehdä groovylla skripti, joka osaa kutsua Jenkinsin hämärästi dokumentoituja rajapintoja, täällä esimerkki: https://github.com/solita/ansible-role-solita.jenkins/blob/master/library/solita_jenkins_user
  3. Pitää ajaa java -jar jenkins-cli.jar ja antaa sille groovy-skripti suoritettavaksi

Lisäksi Jenkins tallettaa konfiguraationsa miten sattuu. Pitkään ajossa olleen Jenkinsin kotihakemistosta löytyivät seuraavat xml-tiedostot, jotka kaikki ovat erilaisia konffisten tallennuspaikkoja: config.xml, credentials.xml, envInject.xml, hudson.maven.MavenModuleSet.xml, hudson.model.UpdateCenter.xml, hudson.plugins.copyartifact.TriggeredBuildSelector.xml, hudson.plugins.git.GitSCM.xml, hudson.plugins.git.GitTool.xml, hudson.plugins.gradle.Gradle.xml, hudson.scm.CVSSCM.xml, hudson.scm.SubversionSCM.xml, hudson.tasks.Ant.xml, hudson.tasks.Mailer.xml, hudson.tasks.Maven.xml, hudson.tasks.Shell.xml, hudson.tools.JDKInstaller.xml, hudson.triggers.SCMTrigger.xml, jenkins.model.ArtifactManagerConfiguration.xml, jenkins.model.DownloadSettings.xml, jenkins.model.JenkinsLocationConfiguration.xml, jenkins.mvn.GlobalMavenConfig.xml, jenkins.plugins.hipchat.HipChatNotifier.xml, jenkins.plugins.slack.SlackNotifier.xml, jenkins.security.QueueItemAuthenticatorConfiguration.xml, nodeMonitors.xml, org.jenkinsci.plugins.gitclient.JGitTool.xml, org.jenkinsci.plugins.xvfb.XvfbBuildWrapper.xml, org.spootnik.LeiningenBuilder.xml, scm-sync-configuration.xml.

9.8.2016

File.renameTo(file) palauttaa virhetilanteessa arvon false. Ei ole mitään keinoa selvittää, mikä meni vikaan.

Tiedostojen uudelleennimeämiseen on parempia tapoja, mutta vanhoista ei ilmeisesti koskaan päästä eroon.

8.8.2016

String.split(regex) heittää oletuksena tulostaulukon lopussa olevat tyhjät merkkijonot pois. Esimerkiksi tekstin rivejä ei voi laskea teksti.split("\n").length, koska tekstin lopussa olevat tyhjät rivit jätetään huomiotta.

Jos haluaa tyhjät merkkijonot mukaan, pitää sanoa teksti.split(..., n) jossa n on mikä tahansa negatiivinen luku. :O

1.6.2016

Ei ole täysin yleispätevää tapaa hakea resurssia CLASSPATH:sta InputStream:ksi siten, että se toimii sekä:

  1. kun resurssi on tiedosto hakemistossa
  2. kun resurssi on .jar:n sisällä

Tässä on clojure-koodia, joka tekee sen oletuksella, että säikeen classloader on "se oikea":

(defn resource->inputstream [resource]
  (.getResourceAsStream
    (.getContextClassLoader (Thread/currentThread))
    resource))


Pikalinkit:


kommentoi (viimeksi muutettu 01.03.2017 15:34)