使用注解解決ShardingJdbc不支持復(fù)雜SQL方法
背景介紹
公司最近做分庫分表業(yè)務(wù),接入了 Sharding JDBC,接入完成后,回歸測試時(shí)發(fā)現(xiàn)好幾個(gè) SQL 執(zhí)行報(bào)錯(cuò),關(guān)鍵這幾個(gè)表都還不是分片表。
報(bào)錯(cuò)如下:
這下糟了嘛。熟悉 Sharding JDBC 的同學(xué)應(yīng)該知道,有很多 SQL 它是不支持的。官方截圖如下:
如果要去修改這些復(fù)雜 SQL 的話,可能要花費(fèi)很多時(shí)間。那怎么辦呢?只能從 Sharding JDBC 這里找突破口了,兩天的研究,出來了下面這個(gè)只需要加一個(gè)注解輕松解決 Sharding Jdbc 不支持復(fù)雜 SQL 的方案。
問題復(fù)現(xiàn)
我本地寫了一個(gè)復(fù)雜 SQL 進(jìn)行測試:
public List<Map<String, Object>> queryOrder(){ List<Map<String, Object>> orders = borderRepository.findOrders(); return orders; }
public interface BOrderRepository extends JpaRepository<BOrder,Long> { @Query(value = "SELECT * FROM (SELECT id,CASE WHEN company_id =1 THEN '小' WHEN company_id=4 THEN '中' ELSE '大' END AS com,user_id as userId FROM b_order0) t WHERE t.com ='中'",nativeQuery =true) List<Map<String, Object>> findOrders(); }
寫了個(gè)測試 controller 來調(diào)用,調(diào)用后果然報(bào)錯(cuò)了。
解決思路
因?yàn)椴樵兊膹?fù)雜 SQL 的表不是分片表,那能不能指定這幾個(gè)復(fù)雜查詢的時(shí)候不用 Sharding JDBC 的數(shù)據(jù)源呢?
- 在注入 Sharding JDBC 數(shù)據(jù)源的地方做處理,注入一個(gè)我們自定義的數(shù)據(jù)源
- 這樣我們獲取連接的時(shí)候就能返回原生數(shù)據(jù)源了
- 另外我們聲明一個(gè)注解,對(duì)標(biāo)識(shí)了注解的就返回原生數(shù)據(jù)源,否則還是返回 Sharding 數(shù)據(jù)源
具體實(shí)現(xiàn)
編寫autoConfig 類
- 編寫一個(gè) autoConfig 類,來替換 ShardingSphereAutoConfiguration 類
/** * 動(dòng)態(tài)數(shù)據(jù)源核心自動(dòng)配置類 * * */ @Configuration @ComponentScan("org.apache.shardingsphere.spring.boot.converter") @EnableConfigurationProperties(SpringBootPropertiesConfiguration.class) @ConditionalOnProperty(prefix = "spring.shardingsphere", name = "enabled", havingValue = "true", matchIfMissing = true) @AutoConfigureBefore(DataSourceAutoConfiguration.class) public class DynamicDataSourceAutoConfiguration implements EnvironmentAware { private String databaseName; private final SpringBootPropertiesConfiguration props; private final Map<String, DataSource> dataSourceMap = new LinkedHashMap<>(); public DynamicDataSourceAutoConfiguration(SpringBootPropertiesConfiguration props) { this.props = props; } /** * Get mode configuration. * * @return mode configuration */ @Bean public ModeConfiguration modeConfiguration() { return null == props.getMode() ? null : new ModeConfigurationYamlSwapper().swapToObject(props.getMode()); } /** * Get ShardingSphere data source bean. * * @param rules rules configuration * @param modeConfig mode configuration * @return data source bean * @throws SQLException SQL exception */ @Bean @Conditional(LocalRulesCondition.class) @Autowired(required = false) public DataSource shardingSphereDataSource(final ObjectProvider<List<RuleConfiguration>> rules, final ObjectProvider<ModeConfiguration> modeConfig) throws SQLException { Collection<RuleConfiguration> ruleConfigs = Optional.ofNullable(rules.getIfAvailable()).orElseGet(Collections::emptyList); DataSource dataSource = ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig.getIfAvailable(), dataSourceMap, ruleConfigs, props.getProps()); return new WrapShardingDataSource((ShardingSphereDataSource) dataSource,dataSourceMap); } /** * Get data source bean from registry center. * * @param modeConfig mode configuration * @return data source bean * @throws SQLException SQL exception */ @Bean @ConditionalOnMissingBean(DataSource.class) public DataSource dataSource(final ModeConfiguration modeConfig) throws SQLException { DataSource dataSource = !dataSourceMap.isEmpty() ? ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig, dataSourceMap, Collections.emptyList(), props.getProps()) : ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig); return new WrapShardingDataSource((ShardingSphereDataSource) dataSource,dataSourceMap); } /** * Create transaction type scanner. * * @return transaction type scanner */ @Bean public TransactionTypeScanner transactionTypeScanner() { return new TransactionTypeScanner(); } @Override public final void setEnvironment(final Environment environment) { dataSourceMap.putAll(DataSourceMapSetter.getDataSourceMap(environment)); databaseName = DatabaseNameSetter.getDatabaseName(environment); } @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Bean @ConditionalOnProperty(prefix = "spring.datasource.dynamic.aop", name = "enabled", havingValue = "true", matchIfMissing = true) public Advisor dynamicDatasourceAnnotationAdvisor() { DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(true); DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class); return advisor; } }
自定義數(shù)據(jù)源
public class WrapShardingDataSource extends AbstractDataSourceAdapter implements AutoCloseable{ private ShardingSphereDataSource dataSource; private Map<String, DataSource> dataSourceMap; public WrapShardingDataSource(ShardingSphereDataSource dataSource, Map<String, DataSource> dataSourceMap) { this.dataSource = dataSource; this.dataSourceMap = dataSourceMap; } public DataSource getTargetDataSource(){ String peek = DynamicDataSourceContextHolder.peek(); if(StringUtils.isEmpty(peek)){ return dataSource; } return dataSourceMap.get(peek); } @Override public Connection getConnection() throws SQLException { return getTargetDataSource().getConnection(); } @Override public Connection getConnection(final String username, final String password) throws SQLException { return getConnection(); } @Override public void close() throws Exception { DataSource targetDataSource = getTargetDataSource(); if (targetDataSource instanceof AutoCloseable) { ((AutoCloseable) targetDataSource).close(); } } @Override public int getLoginTimeout() throws SQLException { DataSource targetDataSource = getTargetDataSource(); return targetDataSource ==null ? 0 : targetDataSource.getLoginTimeout(); } @Override public void setLoginTimeout(final int seconds) throws SQLException { DataSource targetDataSource = getTargetDataSource(); targetDataSource.setLoginTimeout(seconds); } }
- 聲明指定數(shù)據(jù)源注解
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS { /** * 數(shù)據(jù)源名 */ String value(); }
- 另外使用 AOP 的方式攔截使用了注解的類或方法,并且要將這些用了注解的方法存起來,在獲取數(shù)據(jù)源連接的時(shí)候取出來進(jìn)行判斷。這就還要用到 ThreadLocal。
aop 攔截器:
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { private final DataSourceClassResolver dataSourceClassResolver; public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly) { dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly); } @Override public Object invoke(MethodInvocation invocation) throws Throwable { String dsKey = determineDatasourceKey(invocation); DynamicDataSourceContextHolder.push(dsKey); try { return invocation.proceed(); } finally { DynamicDataSourceContextHolder.poll(); } } private String determineDatasourceKey(MethodInvocation invocation) { String key = dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis()); return key; } }
aop 切面定義
/** * aop Advisor */ public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { private final Advice advice; private final Pointcut pointcut; private final Class<? extends Annotation> annotation; public DynamicDataSourceAnnotationAdvisor(MethodInterceptor advice, Class<? extends Annotation> annotation) { this.advice = advice; this.annotation = annotation; this.pointcut = buildPointcut(); } @Override public Pointcut getPointcut() { return this.pointcut; } @Override public Advice getAdvice() { return this.advice; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (this.advice instanceof BeanFactoryAware) { ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); } } private Pointcut buildPointcut() { Pointcut cpc = new AnnotationMatchingPointcut(annotation, true); Pointcut mpc = new AnnotationMethodPoint(annotation); return new ComposablePointcut(cpc).union(mpc); } /** * In order to be compatible with the spring lower than 5.0 */ private static class AnnotationMethodPoint implements Pointcut { private final Class<? extends Annotation> annotationType; public AnnotationMethodPoint(Class<? extends Annotation> annotationType) { Assert.notNull(annotationType, "Annotation type must not be null"); this.annotationType = annotationType; } @Override public ClassFilter getClassFilter() { return ClassFilter.TRUE; } @Override public MethodMatcher getMethodMatcher() { return new AnnotationMethodMatcher(annotationType); } private static class AnnotationMethodMatcher extends StaticMethodMatcher { private final Class<? extends Annotation> annotationType; public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) { this.annotationType = annotationType; } @Override public boolean matches(Method method, Class<?> targetClass) { if (matchesMethod(method)) { return true; } // Proxy classes never have annotations on their redeclared methods. if (Proxy.isProxyClass(targetClass)) { return false; } // The method may be on an interface, so let's check on the target class as well. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); return (specificMethod != method && matchesMethod(specificMethod)); } private boolean matchesMethod(Method method) { return AnnotatedElementUtils.hasAnnotation(method, this.annotationType); } } } }
/** * 數(shù)據(jù)源解析器 * */ public class DataSourceClassResolver { private static boolean mpEnabled = false; private static Field mapperInterfaceField; static { Class<?> proxyClass = null; try { proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.MybatisMapperProxy"); } catch (ClassNotFoundException e1) { try { proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.PageMapperProxy"); } catch (ClassNotFoundException e2) { try { proxyClass = Class.forName("org.apache.ibatis.binding.MapperProxy"); } catch (ClassNotFoundException ignored) { } } } if (proxyClass != null) { try { mapperInterfaceField = proxyClass.getDeclaredField("mapperInterface"); mapperInterfaceField.setAccessible(true); mpEnabled = true; } catch (NoSuchFieldException e) { e.printStackTrace(); } } } /** * 緩存方法對(duì)應(yīng)的數(shù)據(jù)源 */ private final Map<Object, String> dsCache = new ConcurrentHashMap<>(); private final boolean allowedPublicOnly; /** * 加入擴(kuò)展, 給外部一個(gè)修改aop條件的機(jī)會(huì) * * @param allowedPublicOnly 只允許公共的方法, 默認(rèn)為true */ public DataSourceClassResolver(boolean allowedPublicOnly) { this.allowedPublicOnly = allowedPublicOnly; } /** * 從緩存獲取數(shù)據(jù) * * @param method 方法 * @param targetObject 目標(biāo)對(duì)象 * @return ds */ public String findKey(Method method, Object targetObject) { if (method.getDeclaringClass() == Object.class) { return ""; } Object cacheKey = new MethodClassKey(method, targetObject.getClass()); String ds = this.dsCache.get(cacheKey); if (ds == null) { ds = computeDatasource(method, targetObject); if (ds == null) { ds = ""; } this.dsCache.put(cacheKey, ds); } return ds; } /** * 查找注解的順序 * 1. 當(dāng)前方法 * 2. 橋接方法 * 3. 當(dāng)前類開始一直找到Object * 4. 支持mybatis-plus, mybatis-spring * * @param method 方法 * @param targetObject 目標(biāo)對(duì)象 * @return ds */ private String computeDatasource(Method method, Object targetObject) { if (allowedPublicOnly && !Modifier.isPublic(method.getModifiers())) { return null; } //1. 從當(dāng)前方法接口中獲取 String dsAttr = findDataSourceAttribute(method); if (dsAttr != null) { return dsAttr; } Class<?> targetClass = targetObject.getClass(); Class<?> userClass = ClassUtils.getUserClass(targetClass); // JDK代理時(shí), 獲取實(shí)現(xiàn)類的方法聲明. method: 接口的方法, specificMethod: 實(shí)現(xiàn)類方法 Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass); specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); //2. 從橋接方法查找 dsAttr = findDataSourceAttribute(specificMethod); if (dsAttr != null) { return dsAttr; } // 從當(dāng)前方法聲明的類查找 dsAttr = findDataSourceAttribute(userClass); if (dsAttr != null && ClassUtils.isUserLevelMethod(method)) { return dsAttr; } //since 3.4.1 從接口查找,只取第一個(gè)找到的 for (Class<?> interfaceClazz : ClassUtils.getAllInterfacesForClassAsSet(userClass)) { dsAttr = findDataSourceAttribute(interfaceClazz); if (dsAttr != null) { return dsAttr; } } // 如果存在橋接方法 if (specificMethod != method) { // 從橋接方法查找 dsAttr = findDataSourceAttribute(method); if (dsAttr != null) { return dsAttr; } // 從橋接方法聲明的類查找 dsAttr = findDataSourceAttribute(method.getDeclaringClass()); if (dsAttr != null && ClassUtils.isUserLevelMethod(method)) { return dsAttr; } } return getDefaultDataSourceAttr(targetObject); } /** * 默認(rèn)的獲取數(shù)據(jù)源名稱方式 * * @param targetObject 目標(biāo)對(duì)象 * @return ds */ private String getDefaultDataSourceAttr(Object targetObject) { Class<?> targetClass = targetObject.getClass(); // 如果不是代理類, 從當(dāng)前類開始, 不斷的找父類的聲明 if (!Proxy.isProxyClass(targetClass)) { Class<?> currentClass = targetClass; while (currentClass != Object.class) { String datasourceAttr = findDataSourceAttribute(currentClass); if (datasourceAttr != null) { return datasourceAttr; } currentClass = currentClass.getSuperclass(); } } // mybatis-plus, mybatis-spring 的獲取方式 if (mpEnabled) { final Class<?> clazz = getMapperInterfaceClass(targetObject); if (clazz != null) { String datasourceAttr = findDataSourceAttribute(clazz); if (datasourceAttr != null) { return datasourceAttr; } // 嘗試從其父接口獲取 return findDataSourceAttribute(clazz.getSuperclass()); } } return null; } /** * 用于處理嵌套代理 * * @param target JDK 代理類對(duì)象 * @return InvocationHandler 的 Class */ private Class<?> getMapperInterfaceClass(Object target) { Object current = target; while (Proxy.isProxyClass(current.getClass())) { Object currentRefObject = AopProxyUtils.getSingletonTarget(current); if (currentRefObject == null) { break; } current = currentRefObject; } try { if (Proxy.isProxyClass(current.getClass())) { return (Class<?>) mapperInterfaceField.get(Proxy.getInvocationHandler(current)); } } catch (IllegalAccessException ignore) { } return null; } /** * 通過 AnnotatedElement 查找標(biāo)記的注解, 映射為 DatasourceHolder * * @param ae AnnotatedElement * @return 數(shù)據(jù)源映射持有者 */ private String findDataSourceAttribute(AnnotatedElement ae) { AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, DS.class); if (attributes != null) { return attributes.getString("value"); } return null; } }
ThreadLocal
public final class DynamicDataSourceContextHolder { /** * 為什么要用鏈表存儲(chǔ)(準(zhǔn)確的是棧) * <pre> * 為了支持嵌套切換,如ABC三個(gè)service都是不同的數(shù)據(jù)源 * 其中A的某個(gè)業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級(jí)一級(jí)調(diào)用切換,形成了鏈。 * 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須使用棧,后進(jìn)先出。 * </pre> */ private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") { @Override protected Deque<String> initialValue() { return new ArrayDeque<>(); } }; private DynamicDataSourceContextHolder() { } /** * 獲得當(dāng)前線程數(shù)據(jù)源 * * @return 數(shù)據(jù)源名稱 */ public static String peek() { return LOOKUP_KEY_HOLDER.get().peek(); } /** * 設(shè)置當(dāng)前線程數(shù)據(jù)源 * <p> * 如非必要不要手動(dòng)調(diào)用,調(diào)用后確保最終清除 * </p> * * @param ds 數(shù)據(jù)源名稱 */ public static String push(String ds) { String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds; LOOKUP_KEY_HOLDER.get().push(dataSourceStr); return dataSourceStr; } /** * 清空當(dāng)前線程數(shù)據(jù)源 * <p> * 如果當(dāng)前線程是連續(xù)切換數(shù)據(jù)源 只會(huì)移除掉當(dāng)前線程的數(shù)據(jù)源名稱 * </p> */ public static void poll() { Deque<String> deque = LOOKUP_KEY_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 強(qiáng)制清空本地線程 * <p> * 防止內(nèi)存泄漏,如手動(dòng)調(diào)用了push可調(diào)用此方法確保清除 * </p> */ public static void clear() { LOOKUP_KEY_HOLDER.remove(); } }
啟動(dòng)類配置
引入我們寫的自動(dòng)配置類,排除 ShardingJdbc 的自動(dòng)配置類。
@SpringBootApplication(exclude = ShardingSphereAutoConfiguration.class) @Import({DynamicDataSourceAutoConfiguration.class}) public class ShardingRunApplication { public static void main(String[] args) { SpringApplication.run(ShardingRunApplication.class); } }
最后,我們給之前寫的 Repository 加上注解:
public interface BOrderRepository extends JpaRepository<BOrder,Long> { @DS("slave0") @Query(value = "SELECT * FROM (SELECT id,CASE WHEN company_id =1 THEN '小' WHEN company_id=4 THEN '中' ELSE '大' END AS com,user_id as userId FROM b_order0) t WHERE t.com ='中'",nativeQuery =true) List<Map<String, Object>> findOrders(); }
再次調(diào)用,查詢成功?。?!
以上就是使用注解解決ShardingJdbc不支持復(fù)雜SQL方法的詳細(xì)內(nèi)容,更多關(guān)于ShardingJdbc不支持復(fù)雜SQL的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)替換集合中的元素的方法詳解
這篇文章主要為大家詳細(xì)介紹了Java中實(shí)現(xiàn)替換集合中的元素的常見方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02Java中提供synchronized后為什么還要提供Lock
這篇文章主要介紹了Java中提供synchronized后為什么還要提供Lock,在Java中提供了synchronized關(guān)鍵字來保證只有一個(gè)線程能夠訪問同步代碼塊,下文更多相關(guān)資料需要的小伙伴可以參考一下2022-03-03SpringCloud?eureka(server)微服務(wù)集群搭建過程
這篇文章主要介紹了微服務(wù)SpringCloud-eureka(server)集群搭建,?項(xiàng)目搭建的主要步驟和配置就是創(chuàng)建項(xiàng)目和引入pom依賴,本文通過圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之醫(yī)院心理咨詢問診系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+Spring+Maven+mybatis+Vue+mysql開發(fā)的醫(yī)院心理咨詢問診系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有心理咨詢問診該有的所有功能,感興趣的朋友快來看看吧2022-01-01SpringBoot使用ExceptionHandler做異常處理
這篇文章主要介紹了SpringBoot使用ExceptionHandler做異常處理,這篇文章通過多種方法案例來介紹該項(xiàng)技術(shù)的使用,需要的朋友可以參考下2021-06-06Java用BigDecimal類解決Double類型精度丟失的問題
這篇文章主要介紹了Java用BigDecimal類解決Double類型精度丟失的問題,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12