基于注解的springboot+mybatis的多數(shù)據(jù)源組件的實(shí)現(xiàn)代碼
通常業(yè)務(wù)開(kāi)發(fā)中,我們會(huì)使用到多個(gè)數(shù)據(jù)源,比如,部分?jǐn)?shù)據(jù)存在mysql實(shí)例中,部分?jǐn)?shù)據(jù)是在oracle數(shù)據(jù)庫(kù)中,那這時(shí)候,項(xiàng)目基于springboot和mybatis,其實(shí)只需要配置兩個(gè)數(shù)據(jù)源即可,只需要按照
dataSource -SqlSessionFactory - SqlSessionTemplate配置好就可以了。
如下代碼,首先我們配置一個(gè)主數(shù)據(jù)源,通過(guò)@Primary注解標(biāo)識(shí)為一個(gè)默認(rèn)數(shù)據(jù)源,通過(guò)配置文件中的spring.datasource作為數(shù)據(jù)源配置,生成SqlSessionFactoryBean,最終,配置一個(gè)SqlSessionTemplate。
@Configuration
@MapperScan(basePackages = "com.xxx.mysql.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {
@Bean(name = "primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druid() {
return new DruidDataSource();
}
@Bean(name = "primarySqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return bean.getObject();
}
@Bean("primarySqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sessionFactory) {
return new SqlSessionTemplate(sessionFactory);
}
}
然后,按照相同的流程配置一個(gè)基于oracle的數(shù)據(jù)源,通過(guò)注解配置basePackages掃描對(duì)應(yīng)的包,實(shí)現(xiàn)特定的包下的mapper接口,使用特定的數(shù)據(jù)源。
@Configuration
@MapperScan(basePackages = "com.nbclass.oracle.mapper", sqlSessionFactoryRef = "oracleSqlSessionFactory")
public class OracleDataSourceConfig {
@Bean(name = "oracleDataSource")
@ConfigurationProperties(prefix = "spring.secondary")
public DataSource oracleDruid(){
return new DruidDataSource();
}
@Bean(name = "oracleSqlSessionFactory")
public SqlSessionFactory oracleSqlSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:oracle/mapper/*.xml"));
return bean.getObject();
}
@Bean("oracleSqlSessionTemplate")
public SqlSessionTemplate oracleSqlSessionTemplate(@Qualifier("oracleSqlSessionFactory") SqlSessionFactory sessionFactory) {
return new SqlSessionTemplate(sessionFactory);
}
}
這樣,就實(shí)現(xiàn)了一個(gè)工程下使用多個(gè)數(shù)據(jù)源的功能,對(duì)于這種實(shí)現(xiàn)方式,其實(shí)也足夠簡(jiǎn)單了,但是如果我們的數(shù)據(jù)庫(kù)實(shí)例有很多,并且每個(gè)實(shí)例都主從配置,那這里維護(hù)起來(lái)難免會(huì)導(dǎo)致包名過(guò)多,不夠靈活。
現(xiàn)在考慮實(shí)現(xiàn)一種對(duì)業(yè)務(wù)侵入足夠小,并且能夠在mapper方法粒度上去支持指定數(shù)據(jù)源的方案,那自然而然想到了可以通過(guò)注解來(lái)實(shí)現(xiàn),首先,自定義一個(gè)注解@DBKey:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DBKey {
String DEFAULT = "default"; // 默認(rèn)數(shù)據(jù)庫(kù)節(jié)點(diǎn)
String value() default DEFAULT;
}
思路和上面基于springboot原生的配置的類(lèi)似,首先定義一個(gè)默認(rèn)的數(shù)據(jù)庫(kù)節(jié)點(diǎn),當(dāng)mapper接口方法/類(lèi)沒(méi)有指定任何注解的時(shí)候,默認(rèn)走這個(gè)節(jié)點(diǎn),注解支持傳入value參數(shù)表示選擇的數(shù)據(jù)源節(jié)點(diǎn)名稱(chēng)。至于注解的實(shí)現(xiàn)邏輯,可以通過(guò)反射來(lái)獲取mapper接口方法/類(lèi)的注解值,然后指定特定的數(shù)據(jù)源。
那在什么時(shí)候執(zhí)行這個(gè)操作獲取呢?可以考慮使用spring AOP織入mapper層,在切入點(diǎn)執(zhí)行具體mapper方法之前,將對(duì)應(yīng)的數(shù)據(jù)源配置放入threaLocal中,有了這個(gè)邏輯,立即動(dòng)手實(shí)現(xiàn):
首先,定義一個(gè)db配置的上下文對(duì)象。維護(hù)所有的數(shù)據(jù)源key實(shí)例,以及當(dāng)前線(xiàn)程使用的數(shù)據(jù)源key:
public class DBContextHolder {
private static final ThreadLocal<String> DB_KEY_CONTEXT = new ThreadLocal<>();
//在app啟動(dòng)時(shí)就加載全部數(shù)據(jù)源,不需要考慮并發(fā)
private static Set<String> allDBKeys = new HashSet<>();
public static String getDBKey() {
return DB_KEY_CONTEXT.get();
}
public static void setDBKey(String dbKey) {
//key必須在配置中
if (containKey(dbKey)) {
DB_KEY_CONTEXT.set(dbKey);
} else {
throw new KeyNotFoundException("datasource[" + dbKey + "] not found!");
}
}
public static void addDBKey(String dbKey) {
allDBKeys.add(dbKey);
}
public static boolean containKey(String dbKey) {
return allDBKeys.contains(dbKey);
}
public static void clear() {
DB_KEY_CONTEXT.remove();
}
}
然后,定義切點(diǎn),在切點(diǎn)before方法中,根據(jù)當(dāng)前mapper接口的@@DBKey注解來(lái)選取對(duì)應(yīng)的數(shù)據(jù)源key:
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DSAdvice implements BeforeAdvice {
@Pointcut("execution(* com.xxx..*.repository.*.*(..))")
public void daoMethod() {
}
@Before("daoMethod()")
public void beforeDao(JoinPoint point) {
try {
innerBefore(point, false);
} catch (Exception e) {
logger.error("DefaultDSAdviceException",
"Failed to set database key,please resolve it as soon as possible!", e);
}
}
/**
* @param isClass 攔截類(lèi)還是接口
*/
public void innerBefore(JoinPoint point, boolean isClass) {
String methodName = point.getSignature().getName();
Class<?> clazz = getClass(point, isClass);
//使用默認(rèn)數(shù)據(jù)源
String dbKey = DBKey.DEFAULT;
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
Method method = null;
try {
method = clazz.getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
}
//方法上存在注解,使用方法定義的datasource
if (method.isAnnotationPresent(DBKey.class)) {
DBKey key = method.getAnnotation(DBKey.class);
dbKey = key.value();
} else {
//方法上不存在注解,使用類(lèi)上定義的注解
clazz = method.getDeclaringClass();
if (clazz.isAnnotationPresent(DBKey.class)) {
DBKey key = clazz.getAnnotation(DBKey.class);
dbKey = key.value();
}
}
DBContextHolder.setDBKey(dbKey);
}
private Class<?> getClass(JoinPoint point, boolean isClass) {
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class<?> clazz = target.getClass();
if (!isClass) {
Class<?>[] clazzList = target.getClass().getInterfaces();
if (clazzList == null || clazzList.length == 0) {
throw new MutiDBException("找不到mapper class,methodName =" + methodName);
}
clazz = clazzList[0];
}
return clazz;
}
}
既然在執(zhí)行mapper之前,該mapper接口最終使用的數(shù)據(jù)源已經(jīng)被放入threadLocal中,那么,只需要重寫(xiě)新的路由數(shù)據(jù)源接口邏輯即可:
public class RoutingDatasource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dbKey = DBContextHolder.getDBKey();
return dbKey;
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
for (Object key : targetDataSources.keySet()) {
DBContextHolder.addDBKey(String.valueOf(key));
}
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
另外,我們?cè)诜?wù)啟動(dòng),配置mybatis的時(shí)候,將所有的db配置加載:
@Bean
@ConditionalOnMissingBean(DataSource.class)
@Autowired
public DataSource dataSource(MybatisProperties mybatisProperties) {
Map<Object, Object> dsMap = new HashMap<>(mybatisProperties.getNodes().size());
for (String nodeName : mybatisProperties.getNodes().keySet()) {
dsMap.put(nodeName, buildDataSource(nodeName, mybatisProperties));
DBContextHolder.addDBKey(nodeName);
}
RoutingDatasource dataSource = new RoutingDatasource();
dataSource.setTargetDataSources(dsMap);
if (null == dsMap.get(DBKey.DEFAULT)) {
throw new RuntimeException(
String.format("Default DataSource [%s] not exists", DBKey.DEFAULT));
}
dataSource.setDefaultTargetDataSource(dsMap.get(DBKey.DEFAULT));
return dataSource;
}
@ConfigurationProperties(prefix = "mybatis")
@Data
public class MybatisProperties {
private Map<String, String> params;
private Map<String, Object> nodes;
/**
* mapper文件路徑:多個(gè)location以,分隔
*/
private String mapperLocations = "classpath*:com/iqiyi/xiu/**/mapper/*.xml";
/**
* Mapper類(lèi)所在的base package
*/
private String basePackage = "com.iqiyi.xiu.**.repository";
/**
* mybatis配置文件路徑
*/
private String configLocation = "classpath:mybatis-config.xml";
}
那threadLocal中的key什么時(shí)候進(jìn)行銷(xiāo)毀呢,其實(shí)可以自定義一個(gè)基于mybatis的攔截器,在攔截器中主動(dòng)調(diào)DBContextHolder.clear()方法銷(xiāo)毀這個(gè)key。具體代碼就不貼了。這樣一來(lái),我們就完成了一個(gè)基于注解的支持多數(shù)據(jù)源切換的中間件。
那有沒(méi)有可以?xún)?yōu)化的點(diǎn)呢?其實(shí),可以發(fā)現(xiàn),在獲取mapper接口/所在類(lèi)的注解的時(shí)候,使用了反射來(lái)獲取的,那我們知道一般反射調(diào)用是比較耗性能的,所以可以考慮在這里加個(gè)本地緩存來(lái)優(yōu)化下性能:
private final static Map<String, String> METHOD_CACHE = new ConcurrentHashMap<>();
//....
public void innerBefore(JoinPoint point, boolean isClass) {
String methodName = point.getSignature().getName();
Class<?> clazz = getClass(point, isClass);
//key為類(lèi)名+方法名
String keyString = clazz.toString() + methodName;
//使用默認(rèn)數(shù)據(jù)源
String dbKey = DBKey.DEFAULT;
//如果緩存中已經(jīng)有這個(gè)mapper方法對(duì)應(yīng)的數(shù)據(jù)源的key,那直接設(shè)置
if (METHOD_CACHE.containsKey(keyString)) {
dbKey = METHOD_CACHE.get(keyString);
} else {
Class<?>[] parameterTypes =
((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
Method method = null;
try {
method = clazz.getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
}
//方法上存在注解,使用方法定義的datasource
if (method.isAnnotationPresent(DBKey.class)) {
DBKey key = method.getAnnotation(DBKey.class);
dbKey = key.value();
} else {
clazz = method.getDeclaringClass();
//使用類(lèi)上定義的注解
if (clazz.isAnnotationPresent(DBKey.class)) {
DBKey key = clazz.getAnnotation(DBKey.class);
dbKey = key.value();
}
}
//先放本地緩存
METHOD_CACHE.put(keyString, dbKey);
}
DBContextHolder.setDBKey(dbKey);
}
這樣一來(lái),只有在第一次調(diào)用這個(gè)mapper接口的時(shí)候,才會(huì)走反射調(diào)用的邏輯去獲取對(duì)應(yīng)的數(shù)據(jù)源,后續(xù),都會(huì)走本地緩存,提升了性能。
到此這篇關(guān)于基于注解的springboot+mybatis的多數(shù)據(jù)源組件的實(shí)現(xiàn)代碼的文章就介紹到這了,更多相關(guān)springboot mybatis多數(shù)據(jù)源組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MybatisPlus 不修改全局策略和字段注解如何將字段更新為null
- MyBatis使用注解開(kāi)發(fā)和無(wú)主配置文件開(kāi)發(fā)的情況
- Mybatis常見(jiàn)注解有哪些(總結(jié))
- Mybatis注解增刪改查的實(shí)例代碼
- 解決MyBatis @param注解參數(shù)類(lèi)型錯(cuò)誤異常的問(wèn)題
- 詳解mybatis @SelectProvider 注解
- mybatis3使用@Select等注解實(shí)現(xiàn)增刪改查操作
- MyBatis注解方式之@Update/@Delete使用詳解
- mybatis省略@Param注解操作
- 如何簡(jiǎn)單使用mybatis注解
相關(guān)文章
Spring自帶的校驗(yàn)框架Validation的使用實(shí)例
今天小編就為大家分享一篇關(guān)于Spring自帶的校驗(yàn)框架Validation的使用實(shí)例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
Spring Cloud項(xiàng)目前后端分離跨域的操作
這篇文章主要介紹了Spring Cloud項(xiàng)目前后端分離跨域的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
java實(shí)現(xiàn)將字符串中首字母轉(zhuǎn)換成大寫(xiě),其它全部轉(zhuǎn)換成小寫(xiě)的方法示例
這篇文章主要介紹了java實(shí)現(xiàn)將字符串中首字母轉(zhuǎn)換成大寫(xiě),其它全部轉(zhuǎn)換成小寫(xiě)的方法,涉及java字符串遍歷、轉(zhuǎn)換、拼接等相關(guān)操作技巧,需要的朋友可以參考下2019-06-06
java語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲
這篇文章主要為大家詳細(xì)介紹了java語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
Springboot @Validated和@Valid的區(qū)別及使用詳解
這篇文章主要介紹了Springboot @Validated和@Valid的區(qū)別及使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
通過(guò)Java壓縮JavaScript代碼實(shí)例分享
這篇文章主要介紹了通過(guò)Java壓縮JavaScript代碼實(shí)例分享,具有一定參考價(jià)值,需要的朋友可以了解下。2017-12-12

