Injekto de dependecoj per konstruilo kun Spring kaj Lombok

La pecoj de la puzlo kinuĝas.
  EN, FR
 4 min. legi
 chop
 Ankoraŭ neniu komento
 

En la pasinteco, ni konsilis uzi la konstruilo por injekti dependecoj kun Spring, kaj ni prezentis Lombok. Divenu… Tiuj iras tre bone kune, krom eble por iom da ruzo por uzi la @Value de Spring.

Generi la injektantan konstruilon kun Lombok #

Ni faru neŝanĝeblan servon, kiu injektas ĝiajn dependecojn per sia konstruilo.

 1import org.springframework.stereotype.Service;
 2
 3@Service
 4public class Library {
 5    private final BooksDatabase booksDb;
 6    private final BorrowersDatabase borrowersDb;
 7
 8    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
 9        this.booksDb = booksDb;
10        this.borrowersDb = borrowersDb;
11    }
12}

Per petado de kion ni vidis kun Lombok lasta fojo, pli konciza maniero fari ĉi tion estus:

1import lombok.RequiredArgsConstructor;
2import org.springframework.stereotype.Service;
3
4@Service
5@RequiredArgsConstructor
6public class Library {
7    private final BooksDatabase booksDb;
8    private final BorrowersDatabase borrowersDb;
9}

Finita! Kaj neniu bontenado de la konstruilo estas necesa se ni aldonas aŭ forigas kampojn. Ĉu tio ne estas vera plezuro fidi ĉi tiujn du?

La ĝena kazo de @Value #

Ne blindu #

Lombok havas lombok.Value, Spring havas org.springframework.beans.factory.annotation.Value. Ili tute ne havas la saman celon. En ĉi tiu afiŝo, ni enfokusigos la @Value de Spring.

La problemo #

Por uzi la injekton per konstruilo, ĉio devas trairi la konstruilon. Se necesas agordon, oni devas specifi @Value pri la koncerna parametro.

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5    private final int maxLendingDays;
 6
 7    public Library(
 8      BooksDatabase booksDb,
 9      BorrowersDatabase borrowersDb,
10      @Value("${library.lending.days.max}") maxLendingDays
11    ) {
12        this.booksDb = booksDb;
13        this.borrowersDb = borrowersDb;
14        this.maxLendingDays = maxLendingDays;
15    }
16}

Kiel fari tamen, se Lombok verkas la konstruilo anstataŭ vi?

La solution naïve #

Kiam unue alfrontis ĉi tiu, la instinkto de mia teamo estis fari tion:

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private int maxLendingDays;
9}

Sed kio okazas en ĉi tiu kazo?

Unue, Lombok generos konstruilon, kiu prenas argumenton por ambaŭ final kampoj, sed ignoros la entjeron. La generita kodo aspektus tiel:

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private int maxLendingDays;
 8
 9    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
10        this.booksDb = booksDb;
11        this.borrowersDb = borrowersDb;
12    }
13}

Kiam Spring renkontas ĉi tiun kodon, ĝi instigas la klason en du stadioj:

  1. Ĉar ekzistas konstruilo, ĝi injektas la du necesajn kampojn.
  2. Ĝi tiam uzas la reflektadon por agordi la ne-final, @Value-prinotatan kampon. La tuta profito de la konstruilo-bazitan injekto tiam estas perdita.

La vera solvo #

Ni diris ke la konduto de Lombok povas esti adaptita per lombok.config dosiero. Ĉi tiu estas unu el la plej gravaj kazoj.

Ekzemple, eblas fari Lombok kopii iujn prinotojn de la kampoj al la respondaj parametroj de la konstruilo. Por ĉi tio, simple aldonu la sekvan linion al la agorda dosiero:

lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value

Tiam skribu vian servon tiel:

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private final int maxLendingDays;
9}

La generita kodo devus esti ekvivalenta al la sekva:

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private final int maxLendingDays;
 8
 9    public Library(
10      BooksDatabase booksDb,
11      BorrowersDatabase borrowersDb,
12      @Value("${library.lending.days.max}") maxLendingDays
13    ) {
14        this.booksDb = booksDb;
15        this.borrowersDb = borrowersDb;
16        this.maxLendingDays = maxLendingDays;
17    }
18}

Miaj aliaj konsiloj #

@RequiredArgsConstructor@AllArgsConstructor #

Mi ŝatas @RequiredArgsConstructor, ĉar ĝi ofertas iom da kontrolo pri kiuj kampoj devas esti transdonitaj al la konstruilo. Tamen, por Spring servoj, ĉiuj kampoj devus tiel komencitaj, do @AllArgsConstructor estas tute korekta solvo.

Aldoni @Autowired al generita konstruilo #

Vi eble deziras, ke via konstruilo estas prinotata kun @Autowired. Kvankam laŭvola1, ĝi povas helpi programistojn, kiuj ne konas bone Spring, kompreni kiel la klaso estas ŝarĝita dum rultempo, ekzemple. Ĉi tio povas esti farita per agordo de via Lombok prinoto:

1@Service
2@RequiredArgsConstructor(onConstructor = @__(@Autowired))
3public class Library {
4    // ...
5}

Konkludo #

Mi esperas, ke ĉi tiu mallonga afiŝo helpos vin forigi iujn el la boilerplate kodo necesita por la instanco de viaj Spring servojn.

Ne hezitu dividi viajn konsiletojn!


  1. @Autowired estas necesa sur la konstruilo nur se la klaso havas aliajn konstruilojn. En ĉi tiu kazo, akurate unu konstruilo devas esti prinotata. ↩︎