使用注解解決ShardingJdbc不支持復(fù)雜SQL方法
背景介紹
公司最近做分庫(kù)分表業(yè)務(wù),接入了 Sharding JDBC,接入完成后,回歸測(cè)試時(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 這里找突破口了,兩天的研究,出來(lái)了下面這個(gè)只需要加一個(gè)注解輕松解決 Sharding Jdbc 不支持復(fù)雜 SQL 的方案。
問(wèn)題復(fù)現(xiàn)
我本地寫(xiě)了一個(gè)復(fù)雜 SQL 進(jìn)行測(cè)試:
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();
}
寫(xiě)了個(gè)測(cè)試 controller 來(lái)調(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)
編寫(xiě)autoConfig 類
- 編寫(xiě)一個(gè) autoConfig 類,來(lái)替換 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 的方式攔截使用了注解的類或方法,并且要將這些用了注解的方法存起來(lái),在獲取數(shù)據(jù)源連接的時(shí)候取出來(lái)進(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)前類開(kāi)始一直找到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)前類開(kāi)始, 不斷的找父類的聲明
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;
}
/**
* 通過(guò) 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)類配置
引入我們寫(xiě)的自動(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);
}
}
最后,我們給之前寫(xiě)的 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)替換集合中的元素的常見(jiàn)方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02
Java中提供synchronized后為什么還要提供Lock
這篇文章主要介紹了Java中提供synchronized后為什么還要提供Lock,在Java中提供了synchronized關(guān)鍵字來(lái)保證只有一個(gè)線程能夠訪問(wèn)同步代碼塊,下文更多相關(guān)資料需要的小伙伴可以參考一下2022-03-03
SpringCloud?eureka(server)微服務(wù)集群搭建過(guò)程
這篇文章主要介紹了微服務(wù)SpringCloud-eureka(server)集群搭建,?項(xiàng)目搭建的主要步驟和配置就是創(chuàng)建項(xiàng)目和引入pom依賴,本文通過(guò)圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之醫(yī)院心理咨詢問(wèn)診系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+Spring+Maven+mybatis+Vue+mysql開(kāi)發(fā)的醫(yī)院心理咨詢問(wèn)診系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有心理咨詢問(wèn)診該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01
SpringBoot使用ExceptionHandler做異常處理
這篇文章主要介紹了SpringBoot使用ExceptionHandler做異常處理,這篇文章通過(guò)多種方法案例來(lái)介紹該項(xiàng)技術(shù)的使用,需要的朋友可以參考下2021-06-06
Java用BigDecimal類解決Double類型精度丟失的問(wèn)題
這篇文章主要介紹了Java用BigDecimal類解決Double類型精度丟失的問(wèn)題,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12

