Ett exempel finns här:https://github.com/afedulov/routing-data- källa .
Spring tillhandahåller en variant av DataSource, kallad AbstractRoutingDatasource
. Den kan användas i stället för standardimplementeringar av DataSource och möjliggör en mekanism för att bestämma vilken konkret DataSource som ska användas för varje operation vid körning. Allt du behöver göra är att utöka den och tillhandahålla en implementering av en abstrakt determineCurrentLookupKey
metod. Det här är platsen för att implementera din anpassade logik för att bestämma den konkreta datakällan. Returnerat objekt fungerar som en uppslagsnyckel. Det är vanligtvis en sträng eller en Enum, som används som kvalificering i Spring-konfiguration (detaljer kommer att följa).
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 kanske undrar vad det här DbContextHolder-objektet är och hur vet det vilken DataSource-identifierare som ska returneras? Tänk på att determineCurrentLookupKey
metod kommer att anropas när TransactionsManager begär en anslutning. Det är viktigt att komma ihåg att varje transaktion är "associerad" med en separat tråd. Mer exakt, TransactionsManager binder Connection till den aktuella tråden. För att kunna skicka olika transaktioner till olika måldatakällor måste vi därför se till att varje tråd på ett tillförlitligt sätt kan identifiera vilken datakälla som är avsedd att användas. Detta gör det naturligt att använda ThreadLocal-variabler för att binda specifik datakälla till en tråd och därmed till en transaktion. Så här görs 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 ser kan du också använda en enum som nyckel och Spring kommer att ta hand om att lösa det korrekt baserat på namnet. Associerad DataSource-konfiguration och nycklar kan se ut så här:
....
<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>
Vid det här laget kan du komma på att du gör något sånt här:
@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 styra vilken datakälla som kommer att användas och vidarebefordra förfrågningar som vi vill. Ser bra ut!
...Eller gör det? Först och främst sticker dessa statiska metodanrop till en magisk DbContextHolder ut. De ser ut som att de inte tillhör affärslogiken. Och det gör de inte. Inte nog med att de inte kommunicerar syftet, utan de verkar ömtåliga och felbenägna (vad sägs om att glömma att rengöra dbType). Och vad händer om ett undantag kastas mellan setDbType och cleanDbType? Vi kan inte bara ignorera det. Vi måste vara helt säkra på att vi återställer dbType, annars kan tråd som returneras till ThreadPool vara i ett "trasigt" tillstånd och försöker skriva till en replik i nästa samtal. Så vi behöver det här:
@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());
}
Hoppsan >_<
! Det här ser definitivt inte ut som något jag skulle vilja lägga in i varje skrivskyddad metod. Kan vi göra bättre? Självklart! Detta mönster av "gör något i början av en metod, sedan gör något i slutet" borde ringa en klocka. Aspekter till undsättning!
Tyvärr har det här inlägget redan blivit för långt för att täcka ämnet anpassade aspekter. Du kan följa upp detaljerna om hur du använder aspekter med denna länk .