欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用注解解決ShardingJdbc不支持復(fù)雜SQL方法

 更新時(shí)間:2022年09月13日 15:40:55   作者:女友在高考  
這篇文章主要為大家介紹了使用注解解決ShardingJdbc不支持復(fù)雜SQL方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景介紹

公司最近做分庫分表業(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&lt;String, DataSource&gt; dataSourceMap;
    public WrapShardingDataSource(ShardingSphereDataSource dataSource, Map&lt;String, DataSource&gt; 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中List集合及其遍歷詳解

    java中List集合及其遍歷詳解

    這篇文章主要介紹了java中List集合及其遍歷詳解的相關(guān)資料,需要的朋友可以參考下
    2015-07-07
  • springboot的logback配置源碼解讀

    springboot的logback配置源碼解讀

    這篇文章主要為大家介紹了springboot的logback配置,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11
  • 深入了解JAVA泛型

    深入了解JAVA泛型

    這篇文章主要介紹了JAVA泛型的相關(guān)知識(shí),文中代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • Java實(shí)現(xiàn)替換集合中的元素的方法詳解

    Java實(shí)現(xiàn)替換集合中的元素的方法詳解

    這篇文章主要為大家詳細(xì)介紹了Java中實(shí)現(xiàn)替換集合中的元素的常見方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-02-02
  • IntelliJ IDEA修改編碼的方法步驟

    IntelliJ IDEA修改編碼的方法步驟

    這篇文章主要介紹了IntelliJ IDEA修改編碼的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-08-08
  • Java中提供synchronized后為什么還要提供Lock

    Java中提供synchronized后為什么還要提供Lock

    這篇文章主要介紹了Java中提供synchronized后為什么還要提供Lock,在Java中提供了synchronized關(guān)鍵字來保證只有一個(gè)線程能夠訪問同步代碼塊,下文更多相關(guān)資料需要的小伙伴可以參考一下
    2022-03-03
  • SpringCloud?eureka(server)微服務(wù)集群搭建過程

    SpringCloud?eureka(server)微服務(wù)集群搭建過程

    這篇文章主要介紹了微服務(wù)SpringCloud-eureka(server)集群搭建,?項(xiàng)目搭建的主要步驟和配置就是創(chuàng)建項(xiàng)目和引入pom依賴,本文通過圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-07-07
  • Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之醫(yī)院心理咨詢問診系統(tǒng)的實(shí)現(xiàn)

    Java畢業(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-01
  • SpringBoot使用ExceptionHandler做異常處理

    SpringBoot使用ExceptionHandler做異常處理

    這篇文章主要介紹了SpringBoot使用ExceptionHandler做異常處理,這篇文章通過多種方法案例來介紹該項(xiàng)技術(shù)的使用,需要的朋友可以參考下
    2021-06-06
  • Java用BigDecimal類解決Double類型精度丟失的問題

    Java用BigDecimal類解決Double類型精度丟失的問題

    這篇文章主要介紹了Java用BigDecimal類解決Double類型精度丟失的問題,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下
    2020-12-12

最新評(píng)論