DependencyInjection workshop
Hva er det?
Dependency Injection (DI) er en variant av Inversion of Control prinsippet, hvor ulike ansvarsområder blir flyttet på for å gjøre det lettere å skalere kodebasen.
I en typisk java-applikasjon starter alt i main-metoden, og objekt-grafen bygges gradvis utover etterhvert som man oppretter nye objekter.
F.eks new Controller(new Service(new DAO()))
.
Sånn kodensnutten ovenfor ser ut må alle som vil ha en ny Controller
forholde seg til en Service
og en DAO
.
Disse må trossalt instansieres og sendes inn som parametere.
I en DI/IoC verden fjerner man behovet for å forholde seg til Service
og DAO
objektene ved å flytte ansvaret for å finne disse inn i Controller
.
Java vs Kotlin
Workshoppen var original skrevet for Bekks java-faggruppe, og "hovedbranchene" er derfor basert på java-kode.
Om man ønsker å gjennomføre workshoppen i kotlin, så kan man ta utgangspunkt i kotlin-branchene.
Kotlin-branchene følger samme navnekonvensjon som java-branchene, men med -kotlin
suffixet.
E.g ønsker man å titte på løsningsforslaget til "Del 2" kan man sjekke branchen del2
for java-koden,
og del2-kotlin
for kotlin-koden.
For å starte workshoppen med kotlin kjører man git checkout master-kotlin
før man starter.
Hvordan gjør vi det?
Vi skal lage noe ligner på Spring, men kommer til å gjøre noen forenklinger underveis. Hovedfasene i vårt DI-bibliotek kommer til å bestå av tre faser;
- Finn alle objekter av interesse
- Instansier alle objektene
- Wire-up
Del 1: Basics
Spring kan konfigureres opp på mange ulike måter.
Tidligere var det XML som ble brukt, men i nyere tid har de fleste gått over til å konfigurere opp spring i java-koden sin (@Bean
, @Inject
, @Autowired
etc).
Men selv her her er det flere muligheter, e.g classpath-scanning eller vha konfigurasjons-filer (Eksempel).
For å undersøke classpath-scanning kommer vi til å ta ibruk ett veldig fint lite java-bibliotek: reflections, men det først vi må gjøre er å lage noen java-annotasjoner som vi kan lete etter etterpå.
NB Kjør node setup.js ex1
for å gjøre klar testene
Bean
, @Inject
og @Import
annotasjonen i src/.../annotations/
, f.eks
Oppgave 1.1 Opprett @Retention(value = RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface Bean {
}
Retention
og @Target
er meta-annotasjoner som forklarer java-kompileren hvordan @Bean
annotasjonen kan brukes.
Retention
kan være en av tre verdier; CLASS
, RUNTIME
og SOURCE
. Og sier noe om når informasjon om annotasjon skal være tilgjengelig.
Target
kan ha mange forskjellige verdier, e.g FIELD
, PARAMETER
etc. Og sier noe om hvor annotasjonen kan brukes.
I eksemplet ovenfor bruker vi @Retention(value = RUNTIME)
slik at informasjonen om Bean
er tilgjengelig runtime.
Og vi bruker @Target(value = {ElementType.METHOD})
for å indikere at det kun er lov å bruke annotasjonen på java-metoder.
@Bean
brukes for å markere bønner, mens @Import
brukes for å lage konfigurasjons-filer. Se eksempel i oppgave 1.2.2
DIYStatic.scan(String classpath)
slik at i Ex1Test.finnAlleBeans
kjører.
Oppgave 1.2.1 Implementer reflections biblioteket er bygd opp rundt konseptet "scanner".
Det biblioteket gjør er å titte gjennom hele classpathen, og basert på hvilke "scanners" som er satt opp lagres ulik informasjon om klassene som finnes.
Vi bryr for nå bare om annotasjoner på metoder, så det holder å bruke MethodAnnotationsScanner
.
Baeldung har en fin gjennomgang av hvordan man bruke biblioteket. Spesielt punkt 4.3 kan være nyttig. ;)
DIYStatic.scan(Class root)
slik at i Ex1Test.finnAlleBeansFraRoot
kjører.
Oppgave 1.2.2 Implementer Konfigurasjons-filer:
Eksempel på konfigurasjons-filer:
// no.utgdev.diy.Application.java
class Application {
public static void main(String[] args){
DIY.load(ApplicationConfig.class);
}
}
// no.utgdev.diy.ApplicationConfig.java
@Import(values = { DAOConfig.class })
class ApplicationConfig {
@Bean
public Service service() {
return new Service();
}
}
// no.utgdev.diy.DAOConfig.java
class DAOConfig {
@Bean
public DAO dao() {
return new DAO();
}
}
DIYStatic.instansiate(Set<Method> methods)
slik at Ex2Test.instansiate
kjører
Oppgave 1.3 Implementere NB Kjør node setup.js ex2
for å gjøre klar testene
DIYStatic.wireup(Map<String, Object> namedObjects)
Oppgave 1.4 Implementere Ingen test for denne. :O Men man kan alltid bruke debuggeren til å titte litt på objektene man får tilbake etter DIYStatic.wireup
.
DIYStatic.getBean(Map<String, Object> objects, String name)
slik at Ex3Test.wireup
kjører
Oppgave 1.5 Implementere NB Kjør node setup.js ex3
for å gjøre klar testene
Del 2: Pakk det sammen
NB Kjør node setup.js ex4
for å gjøre klar testene
DIY.load(String classPath)
Oppgave 2.1 Implementer DIY.load(Class root)
Oppgave 2.2 Implementer DIY.getBean(String name)
Oppgave 2.3 Implementer DIY.reset()
Oppgave 2.4 Implementer Etter Oppgave 2.3 burde Ex4Test.run
kjøre. ;)
Del 3: Features
@Named
, og respekter bønner med denne annotasjonen (Ex5Test)
Oppgave 3.1 Opprett NB Kjør node setup.js ex5
for å gjøre klar testene
Noen ganger kan det være greit å ha greit å ha flere bønner av samme, men man trenger da en måte å skille de på. Dette håndteres via å gi alle bønner ett navn (BeanNameGenerator), men for å slippe å holde styr på hvilken navn Spring velger å gi til bønnen vår så kan vi velge å gi den ett spesifikt navn selv.
I Spring brukes gjerne @Qualifier
eller @Named
for dette.
@Inject
@Named("minBønne")
Service service;
@Bean(name = "minBønne")
public Service enEllerAnnenService() {}
Hadde vi i eksemplet ovenfor ikke gitt Service
-bønnen ett eksplisitt navn ville den brukt metode-navnet enEllerAnnenService
som navnet.
La oss se om vi kan få Ex5Test.run
til å kjøre.
@Component
og @Service
annotasjonen i src/.../annotations/
Oppgave 3.2 Opprett NB Kjør node setup.js ex6
for å gjøre klar testene
Spring kommer med ett sett av stereotype-annotasjoner,
disse kan brukes for å annotere klasser slik at de blir automatisk plukket opp ved classpath-scanning,
uten behovet for å skrive egne @Bean
-metoder.
Alle steretype-annotasjonene er en spesialisering av @Component
, men man ønsker ofte å skille mellom de når man skal begynne med aspect-oriented-programming.
Fiks DIY.load(String classPath)
slik at den også laster inn klasser med steretype-annotasjoner, og etter det burde Ex6Test.run
kunne fungere.
@PostConstruct
i src/.../annotations/
Oppgave 3.3 Opprett NB Kjør node setup.js ex7
for å gjøre klar testene
Siden vi bare har implementer field-injection så vil ikke bønnene være klare når en evt konstruktør kjører. Men i blandt vil vi fortsatt gjøre ting når en klasse blir instansiert.
@PostConstruct
lar oss annotere en metode som vi vil at biblioteket skal kjøre etter instansiering og alle bønnene vi trenger har blitt injected.
Sjekk at Ex7Test.run
kjører.