Wenn wir Software entwickeln, wollen wir schaffen „- Wirbelsäule “: Ich sehe Wirbelsäule , Wartbarkeit Wirbelsäule verläuft Wirbelsäule und - in einem Trend jetzt - Zerlegung (die Fähigkeit , den Monolithen auf mikroservisy zu erweitern, falls erforderlich). Fügen Sie die Liste Ihrer Liebling Fähigkeit Wirbelsäule . "
Die meisten - vielleicht sogar alle - dieser "Merkmale" gehen Hand in Hand mit reinen Abhängigkeiten zwischen Komponenten.
Wenn eine Komponente von allen anderen Komponenten abhängt, wissen wir nicht, welche Nebenwirkungen das Ändern einer Komponente haben wird, was die Pflege der Codebasis erschwert und das Erweitern und Zerlegen noch schwieriger macht.
Im Laufe der Zeit verschwimmen die Grenzen der Komponenten in der Codebasis. Es treten fehlerhafte Abhängigkeiten auf, die die Arbeit mit dem Code erschweren. Dies hat alle möglichen schlimmen Konsequenzen. Insbesondere die Entwicklung verlangsamt sich.
Dies ist umso wichtiger, wenn wir an einer monolithischen Codebasis arbeiten, die viele verschiedene Geschäftsbereiche oder "begrenzte Kontexte" umfasst, um die Fachsprache Domain-Driven Design zu verwenden.
Wie können wir unsere Codebasis vor unerwünschten Abhängigkeiten schützen? Mit sorgfältiger Gestaltung begrenzter Kontexte und ständiger Einhaltung von Komponentengrenzen. Dieser Artikel beschreibt eine Reihe von Vorgehensweisen, die Ihnen in beiden Fällen bei der Arbeit mit Spring Boot helfen.
Beispielcode
Dieser Artikel wird von einem Beispielarbeitscode auf GitHub begleitet .
Paket-private Sichtbarkeit
Was hilft bei der Einhaltung von Komponentengrenzen? Reduzierte Sichtbarkeit.
Wenn wir für "innere" Klassen die paketprivate Sichtbarkeit verwenden, haben nur die Klassen im selben Paket Zugriff. Dies macht es schwierig, unerwünschte Abhängigkeiten von außerhalb des Pakets hinzuzufügen.
, , . ?
, .
, .
, , .
! , , . , , , . !
, , package-private , , , .
? package-private . , package-private , , ArchUnit , package-private .
. , , :

. .
Domain-Driven Design (DDD): , . , . «» « » .
, . .
: , . . public , , .
API
, :
billing
├── api
└── internal
├── batchjob
| └── internal
└── database
├── api
└── internal internal, , , , api, , , API, .
internal api :
, internal package-private. public ( public, ), .
, Java package-private , , .
.
Package-Private
database:
database
├── api
| ├── + LineItem
| ├── + ReadLineItems
| └── + WriteLineItems
└── internal
└── o BillingDatabase+, public, o, package-private.
database API ReadLineItems WriteLineItems, , . LineItem API.
database , BillingDatabase :
@Component
class BillingDatabase implements WriteLineItems, ReadLineItems {
...
}, .
, .
api, internal, . internal , , api.
database, , , .
batchjob:
batchjob API . LoadInvoiceDataBatchJob(, , ), , WriteLineItems:
@Component
@RequiredArgsConstructor
class LoadInvoiceDataBatchJob {
private final WriteLineItems writeLineItems;
@Scheduled(fixedRate = 5000)
void loadDataFromBillingSystem() {
...
writeLineItems.saveLineItems(items);
}
} , @Scheduled Spring, .
, billing:
billing
├── api
| ├── + Invoice
| └── + InvoiceCalculator
└── internal
├── batchjob
├── database
└── o BillingService
billing InvoiceCalculator Invoice. , InvoiceCalculator , BillingService . BillingService ReadLineItemsAPI - :
@Component
@RequiredArgsConstructor
class BillingService implements InvoiceCalculator {
private final ReadLineItems readLineItems;
@Override
public Invoice calculateInvoice(
Long userId,
LocalDate fromDate,
LocalDate toDate) {
List<LineItem> items = readLineItems.getLineItemsForUser(
userId,
fromDate,
toDate);
...
}
}, , , .
Spring Boot
, Spring Java Config Configuration internal :
billing
└── internal
├── batchjob
| └── internal
| └── o BillingBatchJobConfiguration
├── database
| └── internal
| └── o BillingDatabaseConfiguration
└── o BillingConfiguration
Spring Spring .
database :
@Configuration
@EnableJpaRepositories
@ComponentScan
class BillingDatabaseConfiguration {
} @Configuration Spring, , Spring .
@ComponentScan Spring, , , ( ) @Component . BillingDatabase, .
@ComponentScan @Bean @Configuration.
database Spring Data JPA. @EnableJpaRepositories.
batchjob :
@Configuration
@EnableScheduling
@ComponentScan
class BillingBatchJobConfiguration {
} @EnableScheduling. , @Scheduled bean-LoadInvoiceDataBatchJob.
, billing :
@Configuration
@ComponentScan
class BillingConfiguration {
} @ComponentScan , @Configuration Spring bean-.
, Spring .
, , @Configuration. , :
()
SpringBootTest.() ,
@Conditional...., , () , () .
: billing.internal.database.api public, billing, .
, ArchUnit.
ArchUnit
ArchUnit - , . , , .
, internal . , billing.internal.*.api billing.internal.
internal , - «».
( «internal» ), , @InternalPackage:
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalPackage {
} package-info.java :
@InternalPackage
package io.reflectoring.boundaries.billing.internal.database.internal;
import io.reflectoring.boundaries.InternalPackage;, , .
, , :
class InternalPackageTests {
private static final String BASE_PACKAGE = "io.reflectoring";
private final JavaClasses analyzedClasses =
new ClassFileImporter().importPackages(BASE_PACKAGE);
@Test
void internalPackagesAreNotAccessedFromOutside() throws IOException {
List<String> internalPackages = internalPackages(BASE_PACKAGE);
for (String internalPackage : internalPackages) {
assertPackageIsNotAccessedFromOutside(internalPackage);
}
}
private List<String> internalPackages(String basePackage) {
Reflections reflections = new Reflections(basePackage);
return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()
.map(c -> c.getPackage().getName())
.collect(Collectors.toList());
}
void assertPackageIsNotAccessedFromOutside(String internalPackage) {
noClasses()
.that()
.resideOutsideOfPackage(packageMatcher(internalPackage))
.should()
.dependOnClassesThat()
.resideInAPackage(packageMatcher(internalPackage))
.check(analyzedClasses);
}
private String packageMatcher(String fullyQualifiedPackage) {
return fullyQualifiedPackage + "..";
}
} internalPackages(), reflection , @InternalPackage.
assertPackageIsNotAccessedFromOutside(). API- ArchUnit, DSL, , «, , , ».
, - public .
: , (io.reflectoring ) ?
, ( ) io.reflectoring. , .
, .
, :
class InternalPackageTests {
private static final String BASE_PACKAGE = "io.reflectoring";
@Test
void internalPackagesAreNotAccessedFromOutside() throws IOException {
// make it refactoring-safe in case we're renaming the base package
assertPackageExists(BASE_PACKAGE);
List<String> internalPackages = internalPackages(BASE_PACKAGE);
for (String internalPackage : internalPackages) {
// make it refactoring-safe in case we're renaming the internal package
assertPackageIsNotAccessedFromOutside(internalPackage);
}
}
void assertPackageExists(String packageName) {
assertThat(analyzedClasses.containPackage(packageName))
.as("package %s exists", packageName)
.isTrue();
}
private List<String> internalPackages(String basePackage) {
...
}
void assertPackageIsNotAccessedFromOutside(String internalPackage) {
...
}
} assertPackageExists() ArchUnit, , , .
. , , . , @InternalPackage internalPackages().
, .
Java- Spring Boot ArchUnit , - .
API , .
!
, , GitHub .
Spring Boot, moduliths.