Et eksempel kan findes her:https://github.com/afedulov/routing-data- kilde .
Spring giver en variation af DataSource, kaldet AbstractRoutingDatasource
. Den kan bruges i stedet for standard DataSource-implementeringer og muliggør en mekanisme til at bestemme, hvilken konkret DataSource der skal bruges til hver operation under kørsel. Alt du skal gøre er at udvide det og levere en implementering af en abstrakt determineCurrentLookupKey
metode. Dette er stedet for at implementere din brugerdefinerede logik for at bestemme den konkrete datakilde. Returneret objekt fungerer som en opslagsnøgle. Det er typisk en streng eller en Enum, der bruges som en kvalifikation i Spring-konfiguration (detaljer følger).
package website.fedulov.routing.RoutingDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
Du undrer dig måske over, hvad det DbContextHolder-objekt er, og hvordan ved det, hvilken DataSource-id, der skal returneres? Husk at determineCurrentLookupKey
metode vil blive kaldt, når TransactionsManager anmoder om en forbindelse. Det er vigtigt at huske, at hver transaktion er "associeret" med en separat tråd. Mere præcist binder TransactionsManager Connection til den aktuelle tråd. For at sende forskellige transaktioner til forskellige måldatakilder skal vi derfor sikre os, at hver tråd pålideligt kan identificere, hvilken datakilde der er beregnet til at blive brugt. Dette gør det naturligt at bruge ThreadLocal-variabler til at binde specifik datakilde til en tråd og dermed til en transaktion. Sådan gøres det:
public enum DbType {
MASTER,
REPLICA1,
}
public class DbContextHolder {
private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();
public static void setDbType(DbType dbType) {
if(dbType == null){
throw new NullPointerException();
}
contextHolder.set(dbType);
}
public static DbType getDbType() {
return (DbType) contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}
Som du kan se, kan du også bruge en enum som nøgle, og Spring vil sørge for at løse det korrekt baseret på navnet. Tilknyttet DataSource-konfiguration og nøgler kan se sådan ud:
....
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource">
<property name="targetDataSources">
<map key-type="com.sabienzia.routing.DbType">
<entry key="MASTER" value-ref="dataSourceMaster"/>
<entry key="REPLICA1" value-ref="dataSourceReplica"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMaster"/>
</bean>
<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${db.master.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${db.replica.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
På dette tidspunkt kan du finde på at gøre noget som dette:
@Service
public class BookService {
private final BookRepository bookRepository;
private final Mapper mapper;
@Inject
public BookService(BookRepository bookRepository, Mapper mapper) {
this.bookRepository = bookRepository;
this.mapper = mapper;
}
@Transactional(readOnly = true)
public Page<BookDTO> getBooks(Pageable p) {
DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key
// all connection from here will go to REPLICA1
Page<Book> booksPage = callActionRepo.findAll(p);
List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
DbContextHolder.clearDbType(); // <----- clear ThreadLocal setting
return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
}
...//other methods
Nu kan vi kontrollere, hvilken DataSource der vil blive brugt og videresende anmodninger, som vi vil. Ser godt ud!
...Eller gør det? Først og fremmest stikker disse statiske metodekald til en magisk DbContextHolder virkelig ud. De ser ud som om de ikke tilhører forretningslogikken. Og det gør de ikke. Ikke alene kommunikerer de ikke formålet, men de virker skrøbelige og fejltilbøjelige (hvad med at glemme at rense dbType). Og hvad hvis der kastes en undtagelse mellem setDbType og cleanDbType? Vi kan ikke bare ignorere det. Vi skal være helt sikre på, at vi nulstiller dbType, ellers kan Tråd returneret til ThreadPool være i en "brudt" tilstand og forsøge at skrive til en replika i det næste opkald. Så vi har brug for dette:
@Transactional(readOnly = true)
public Page<BookDTO> getBooks(Pageable p) {
try{
DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key
// all connection from here will go to REPLICA1
Page<Book> booksPage = callActionRepo.findAll(p);
List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
DbContextHolder.clearDbType(); // <----- clear ThreadLocal setting
} catch (Exception e){
throw new RuntimeException(e);
} finally {
DbContextHolder.clearDbType(); // <----- make sure ThreadLocal setting is cleared
}
return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
}
Yikes >_<
! Dette ligner bestemt ikke noget, jeg gerne vil indsætte i enhver skrivebeskyttet metode. Kan vi gøre det bedre? Selvfølgelig! Dette mønster med "gør noget i begyndelsen af en metode, så gør noget til sidst" burde ringe en klokke. Aspekter til undsætning!
Desværre er dette indlæg allerede blevet for langt til at dække emnet tilpassede aspekter. Du kan følge op på detaljerne om brug af aspekter ved hjælp af denne link .