在 Spring 里有这样一个抽象类 AbstractRoutingDataSource,这个类相当于 DataSource 的路由中介,在运行时根据某种 key 值来动态切换到所需的 DataSource 上。通过实现这个类就可以实现我们期望的动态数据源切换。
这里强调一下,这个类里有这么几个关键属性:
targetDataSources 保存了 key 和数据库连接的映射关系;
defaultTargetDataSource 标识默认的连接;
resolvedDataSources 存储数据库标识和数据源的映射关系。
1,首先,我们创建一个 MyDataSource 类,继承了 AbstractRoutingDataSource,并覆写了 determineCurrentLookupKey():
public class MyDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> key = new ThreadLocal<String>();
@Override
protected Object determineCurrentLookupKey() {
return key.get();
}
public static void setDataSource(String dataSource) {
key.set(dataSource);
}
public static String getDatasource() {
return key.get();
}
public static void clearDataSource() {
key.remove();
}
}
2,其次,我们需要修改 JdbcConfig。这里我新写了一个 dataSource,将原来的 dataSource 改成 dataSourceCore,再将新定义的 dataSourceCore 和 dataSourceCard 放进一个 Map,对应的 key 分别是 core 和 card,并把 Map 赋值给 setTargetDataSources
public class JdbcConfig {
//省略非关键代码
@Value("${card.driver}")
private String cardDriver;
@Value("${card.url}")
private String cardUrl;
@Value("${card.username}")
private String cardUsername;
@Value("${card.password}")
private String cardPassword;
@Autowired
@Qualifier("dataSourceCard")
private DataSource dataSourceCard;
@Autowired
@Qualifier("dataSourceCore")
private DataSource dataSourceCore;
//省略非关键代码
@Bean(name = "dataSourceCore")
public DataSource createCoreDataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
@Bean(name = "dataSourceCard")
public DataSource createCardDataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(cardDriver);
ds.setUrl(cardUrl);
ds.setUsername(cardUsername);
ds.setPassword(cardPassword);
return ds;
}
@Bean(name = "dataSource")
public MyDataSource createDataSource() {
MyDataSource myDataSource = new MyDataSource();
Map<Object, Object> map = new HashMap<>();
map.put("core", dataSourceCore);
map.put("card", dataSourceCard);
myDataSource.setTargetDataSources(map);
myDataSource.setDefaultTargetDataSource(dataSourceCore);
return myDataSource;
}
//省略非关键代码
}
3.触发setDataSource 这个方法什么时候执行
我们定义了一个新的注解 @DataSource,可以直接加在 Service() 上,实现数据库切换:
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value();
String core = "core";
String card = "card";
}
// 使用方式
@DataSource(DataSource.card)
另外,我们还需要写一个 Spring AOP 来对相应的服务方法进行拦截,完成数据源的切换操作。特别要注意的是,这里要加上一个 @Order(1) 标记它的初始化顺序。这个 Order 值一定要比事务的 AOP 切面的值小,这样可以获得更高的优先级,否则自动切换数据源将会失效。
@Aspect
@Service
@Order(1)
public class DataSourceSwitch {
@Around("execution(* com.spring.puzzle.others.transaction.example3.CardService.*(..))")
public void around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(DataSource.class)) {
DataSource dataSource = method.getAnnotation(DataSource.class);
MyDataSource.setDataSource(dataSource.value());
System.out.println("数据源切换至:" + MyDataSource.getDatasource());
}
point.proceed();
MyDataSource.clearDataSource();
System.out.println("数据源已移除!");
}
}
最后调用
@Service
public class CardService {
@Autowired
private CardMapper cardMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@DataSource(DataSource.card)
public void createCard(int studentId) throws Exception {
Card card = new Card();
card.setStudentId(studentId);
card.setBalance(50);
cardMapper.saveCard(card);
}
}
@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
Student student = new Student();
student.setRealname(realname);
studentService.doSaveStudent(student);
try {
courseService.regCourse(student.getId());
cardService.createCard(student.getId());
} catch (Exception e) {
e.printStackTrace();
}
}