Mybatis操作多數(shù)據(jù)源的實現(xiàn)
現(xiàn)在有一個Mysql數(shù)據(jù)源和一個Postgresql數(shù)據(jù)源,使用Mybatis對兩個數(shù)據(jù)源進(jìn)行操作:
1. 注入多數(shù)據(jù)源
可以對兩個數(shù)據(jù)源分別實現(xiàn)其Service層和Mapper層,以及Mybatis的配置類:
@Configuration // 這里需要配置掃描包路徑,以及sqlSessionTemplateRef @MapperScan(basePackages = "com.example.mybatisdemo.mapper.mysql", sqlSessionTemplateRef = "mysqlSqlSessionTemplate") public class MysqlMybatisConfigurer { ? ? /** ? ? ?* 注入Mysql數(shù)據(jù)源 ? ? ?*/ ? ? @Bean ? ? @ConfigurationProperties(prefix = "spring.datasource.mysql") ? ? public DataSource mysqlDatasource() { ? ? ? ? return new DruidDataSource(); ? ? } ? ? /** ? ? ?* 注入mysqlSqlSessionFactory ? ? ?*/ ? ? @Bean ? ? public SqlSessionFactory mysqlSqlSessionFactory(DataSource mysqlDatasource) throws Exception { ? ? ? ? SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); ? ? ? ? factoryBean.setDataSource(mysqlDatasource); ? ? ? ? // 設(shè)置對應(yīng)的mapper文件 ? ? ? ? factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" + ? ? ? ? ? ? ? ? "/mappers/MysqlMapper.xml")); ? ? ? ? return factoryBean.getObject(); ? ? } ? ? /** ? ? ?* 注入mysqlSqlSessionTemplate ? ? ?*/ ? ? @Bean ? ? public SqlSessionTemplate mysqlSqlSessionTemplate(SqlSessionFactory mysqlSqlSessionFactory) { ? ? ? ? return new SqlSessionTemplate(mysqlSqlSessionFactory); ? ? } ? ? /** ? ? ?* 注入mysqlTransactionalManager ? ? ?*/ ? ? @Bean ? ? public DataSourceTransactionManager mysqlTransactionalManager(DataSource mysqlDatasource) { ? ? ? ? return new DataSourceTransactionManager(mysqlDatasource); ? ? } }
@Configuration // 這里需要配置掃描包路徑,以及sqlSessionTemplateRef @MapperScan(basePackages = "com.example.mybatisdemo.mapper.postgresql", sqlSessionTemplateRef = "postgresqlSqlSessionTemplate") public class PostgresqlMybatisConfigurer { ? ? /** ? ? ?* 注入Postgresql數(shù)據(jù)源 ? ? ?*/ ? ? @Bean ? ? @ConfigurationProperties(prefix = "spring.datasource.postgresql") ? ? public DataSource postgresqlDatasource() { ? ? ? ? return new DruidDataSource(); ? ? } ? ? /** ? ? ?* 注入postgresqlSqlSessionFactory ? ? ?*/ ? ? @Bean ? ? public SqlSessionFactory postgresqlSqlSessionFactory(DataSource postgresqlDatasource) throws Exception { ? ? ? ? SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); ? ? ? ? factoryBean.setDataSource(postgresqlDatasource); ? ? ? ? // 設(shè)置對應(yīng)的mapper文件 ? ? ? ? factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" + ? ? ? ? ? ? ? ? "/mappers/PostgresqlMapper.xml")); ? ? ? ? return factoryBean.getObject(); ? ? } ? ? /** ? ? ?* 注入postgresqlSqlSessionTemplate ? ? ?*/ ? ? @Bean ? ? public SqlSessionTemplate postgresqlSqlSessionTemplate(SqlSessionFactory postgresqlSqlSessionFactory) { ? ? ? ? return new SqlSessionTemplate(postgresqlSqlSessionFactory); ? ? } ? ? /** ? ? ?* 注入postgresqlTransactionalManager ? ? ?*/ ? ? @Bean ? ? public DataSourceTransactionManager postgresqlTransactionalManager(DataSource postgresqlDatasource) { ? ? ? ? return new DataSourceTransactionManager(postgresqlDatasource); ? ? } }
在配置類中,分別注入了一個事務(wù)管理器TransactionManager,這個和事務(wù)管理是相關(guān)的。在使用@Transactional注解時,需要配置其value屬性指定對應(yīng)的事務(wù)管理器。
2. 動態(tài)數(shù)據(jù)源
Spring中提供了AbstractRoutingDataSource抽象類,可以用于動態(tài)地選擇數(shù)據(jù)源。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { @Nullable private Map<Object, Object> targetDataSources; @Nullable private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); @Nullable private Map<Object, DataSource> resolvedDataSources; @Nullable private DataSource resolvedDefaultDataSource; // 略 }
通過源碼可以看到,該抽象類實現(xiàn)了InitializingBean接口,并在其afterPropertiesSet方法中將數(shù)據(jù)源以<lookupkey, dataSource>的形式放入一個Map中。
public void afterPropertiesSet() { ? ? if (this.targetDataSources == null) { ? ? ? ? throw new IllegalArgumentException("Property 'targetDataSources' is required"); ? ? } else { ? ? ? ? this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size()); ? ? ? ? this.targetDataSources.forEach((key, value) -> { ? ? ? ? ? ? Object lookupKey = this.resolveSpecifiedLookupKey(key); ? ? ? ? ? ? DataSource dataSource = this.resolveSpecifiedDataSource(value); ? ? ? ? ? ? // 將數(shù)據(jù)源以<lookupkey, dataSource>的形式放入Map中 ? ? ? ? ? ? this.resolvedDataSources.put(lookupKey, dataSource); ? ? ? ? }); ? ? ? ? if (this.defaultTargetDataSource != null) { ? ? ? ? ? ? this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); ? ? ? ? } ? ? } }
該類中還有一個determineTargetDataSource方法,是根據(jù)lookupkey從Map中獲取對應(yīng)的數(shù)據(jù)源,如果沒有獲取到,則使用默認(rèn)的數(shù)據(jù)源。
protected DataSource determineTargetDataSource() { ? ? Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); ? ? Object lookupKey = this.determineCurrentLookupKey(); ? ? // 根據(jù)lookupkey從Map中獲取對應(yīng)的數(shù)據(jù)源 ? ? DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); ? ? if (dataSource == null && (this.lenientFallback || lookupKey == null)) { ? ? ? ? dataSource = this.resolvedDefaultDataSource; ? ? } ? ? if (dataSource == null) { ? ? ? ? throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); ? ? } else { ? ? ? ? return dataSource; ? ? } }
lookupkey是通過determineTargetDataSource方法獲取到的,而它是一個抽象方法,我們要做的就是通過實現(xiàn)這個方法,來控制獲取到的數(shù)據(jù)源。
@Nullable protected abstract Object determineCurrentLookupKey();
(1) 創(chuàng)建并注入動態(tài)數(shù)據(jù)源
創(chuàng)建AbstractRoutingDataSource的子類,實現(xiàn)determineCurrentLookupKey方法
public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } }
這里的DataSourceContextHolder是一個操作ThreadLocal對象的工具類
public class DataSourceContextHolder { ? ? /** ? ? ?* 數(shù)據(jù)源上下文 ? ? ?*/ ? ? private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>(); ? ? /** ? ? ?* 設(shè)置數(shù)據(jù)源類型 ? ? ?*/ ? ? public static void set(DataSourceType type) { ? ? ? ? contextHolder.set(type); ? ? } ? ? /** ? ? ?* 獲取數(shù)據(jù)源類型 ? ? ?* ? ? ?* @return DataSourceType ? ? ?*/ ? ? public static DataSourceType get() { ? ? ? ? return contextHolder.get(); ? ? } ? ? /** ? ? ?* 使用MYSQL數(shù)據(jù)源 ? ? ?*/ ? ? public static void mysql() { ? ? ? ? set(DataSourceType.MYSQL); ? ? } ? ? /** ? ? ?* 使用Postgresql數(shù)據(jù)源 ? ? ?*/ ? ? public static void postgresql() { ? ? ? ? set(DataSourceType.POSTGRESQL); ? ? } ? ? public static void remove() { ? ? ? ? contextHolder.remove(); ? ? } }
通過調(diào)用DataSourceContextHolder.mysql()或者DataSourceContextHolder.postgresql()就能修改contextHolder的值,從而在動態(tài)數(shù)據(jù)源的determineTargetDataSource方法中就能獲取到對應(yīng)的數(shù)據(jù)源。
在數(shù)據(jù)源配置類中,將mysql和postgresql的數(shù)據(jù)源設(shè)置到動態(tài)數(shù)據(jù)源的Map中,并注入容器。
@Configuration public class DataSourceConfigurer { ? ? @Bean ? ? @ConfigurationProperties(prefix = "spring.datasource.mysql") ? ? public DataSource mysqlDatasource() { ? ? ? ? return new DruidDataSource(); ? ? } ? ? @Bean ? ? @ConfigurationProperties(prefix = "spring.datasource.postgresql") ? ? public DataSource postgresqlDatasource() { ? ? ? ? return new DruidDataSource(); ? ? } ? ? @Bean ? ? public RoutingDataSource routingDataSource(DataSource mysqlDatasource, DataSource postgresqlDatasource) { ? ? ? ? Map<Object, Object> dataSources = new HashMap<>(); ? ? ? ? dataSources.put(DataSourceType.MYSQL, mysqlDatasource); ? ? ? ? dataSources.put(DataSourceType.POSTGRESQL, postgresqlDatasource); ? ? ? ? RoutingDataSource routingDataSource = new RoutingDataSource(); ? ? ? ? routingDataSource.setDefaultTargetDataSource(mysqlDatasource); ? ? ? ? // 設(shè)置數(shù)據(jù)源 ? ? ? ? routingDataSource.setTargetDataSources(dataSources); ? ? ? ? return routingDataSource; ? ? } }
(2) Mybatis配置類
由于使用了動態(tài)數(shù)據(jù)源,所以只需要編寫一個配置類即可。
@Configuration @MapperScan(basePackages = "com.example.mybatisdemo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate") public class MybatisConfigurer { ? ? // 注入動態(tài)數(shù)據(jù)源 ? ? @Resource ? ? private RoutingDataSource routingDataSource; ? ? @Bean ? ? public SqlSessionFactory sqlSessionFactory() throws Exception { ? ? ? ? SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); ? ? ? ? sqlSessionFactoryBean.setDataSource(routingDataSource); ? ? ? ? // 這里可以直接設(shè)置所有的mapper.xml文件 ? ? ? ? sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath" + ? ? ? ? ? ? ? ? ":mappers/*.xml")); ? ? ? ? return sqlSessionFactoryBean.getObject(); ? ? } ? ? @Bean ? ? public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ? ? ? ? return new SqlSessionTemplate(sqlSessionFactory); ? ? } ? ? @Bean ? ? public DataSourceTransactionManager transactionalManager(DataSource mysqlDatasource) { ? ? ? ? return new DataSourceTransactionManager(mysqlDatasource); ? ? } }
(3) 使用注解簡化數(shù)據(jù)源切換
我們雖然可以使用DataSourceContextHolder類中的方法進(jìn)行動態(tài)數(shù)據(jù)源切換,但是這種方式有些繁瑣,不夠優(yōu)雅??梢钥紤]使用注解的形式簡化數(shù)據(jù)源切換。
我們先定義兩個注解,表示使用Mysql數(shù)據(jù)源或Postgresql數(shù)據(jù)源:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Mysql { }
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Postgresql { }
再定義一個切面,當(dāng)使用了注解時,會先調(diào)用切換數(shù)據(jù)源的方法,再執(zhí)行后續(xù)邏輯。
@Component @Aspect public class DataSourceAspect { ? ? @Pointcut("@within(com.example.mybatisdemo.aop.Mysql) || @annotation(com.example.mybatisdemo.aop.Mysql)") ? ? public void mysqlPointcut() { ? ? } ? ? @Pointcut("@within(com.example.mybatisdemo.aop.Postgresql) || @annotation(com.example.mybatisdemo.aop.Postgresql)") ? ? public void postgresqlPointcut() { ? ? } ? ? @Before("mysqlPointcut()") ? ? public void mysql() { ? ? ? ? DataSourceContextHolder.mysql(); ? ? } ? ? @Before("postgresqlPointcut()") ? ? public void postgresql() { ? ? ? ? DataSourceContextHolder.postgresql(); ? ? } }
在使用動態(tài)數(shù)據(jù)源的事務(wù)操作時有兩個需要注意的問題:
問題一 同一個事務(wù)操作兩個數(shù)據(jù)源
Mybatis使用Executor執(zhí)行SQL時需要獲取連接,BaseExecutor類中的getConnection方法調(diào)用了SpringManagedTransaction中的getConnection方法,這里優(yōu)先從connection字段獲取連接,如果connection為空,才會調(diào)用openConnection方法,并把連接賦給connection字段。
也就是說,如果你使用的是同一個事務(wù)來操作兩個數(shù)據(jù)源,那拿到的都是同一個連接,會導(dǎo)致數(shù)據(jù)源切換失敗。
protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = this.transaction.getConnection(); return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection; }
public Connection getConnection() throws SQLException { ? ? if (this.connection == null) { ? ? ? ? this.openConnection(); ? ? } ? ? return this.connection; }
private void openConnection() throws SQLException { this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); LOGGER.debug(() -> { return "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"; }); }
問題二 兩個獨立事務(wù)分別操作兩個數(shù)據(jù)源
(1) 在開啟事務(wù)的時候,DataSourceTransactionManager中的doBegin方法會先獲取Connection,并保存到ConnectionHolder中,將數(shù)據(jù)源和ConnectionHolder的對應(yīng)關(guān)系綁定到TransactionSynchronizationManager中。
protected void doBegin(Object transaction, TransactionDefinition definition) { ? ? DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction; ? ? Connection con = null; ? ? try { ? ? ? ? if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { ? ? ? ? ? ? // 獲取連接 ? ? ? ? ? ? Connection newCon = this.obtainDataSource().getConnection(); ? ? ? ? ? ? if (this.logger.isDebugEnabled()) { ? ? ? ? ? ? ? ? this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); ? ? ? ? ? ? } ? ? ? ? ? ? // 保存到ConnectionHolder中 ? ? ? ? ? ? txObject.setConnectionHolder(new ConnectionHolder(newCon), true); ? ? ? ? } ? ? ? ? txObject.getConnectionHolder().setSynchronizedWithTransaction(true); ? ? ? ? // 從ConnectionHolder獲取連接 ? ? ? ? con = txObject.getConnectionHolder().getConnection(); ? ? ? ? // 略 ? ? ? ? // 將數(shù)據(jù)源和ConnectionHolder的關(guān)系綁定到TransactionSynchronizationManager中 ? ? ? ? if (txObject.isNewConnectionHolder()) { ? ? ? ? ? ? TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder()); ? ? ? ? } ? ? ? ? ?// 略 }
(2) TransactionSynchronizationManager的bindResource方法將數(shù)據(jù)源和ConnectionHolder的對應(yīng)關(guān)系存入線程變量resources中。
public abstract class TransactionSynchronizationManager { ? ? // 線程變量 ? ? private static final ThreadLocal<Map<Object, Object>> resources = ? ? ? ? ?new NamedThreadLocal<>("Transactional resources"); ? ? // 略 ? ? // 綁定數(shù)據(jù)源和ConnectionHolder的對應(yīng)關(guān)系 ? ? public static void bindResource(Object key, Object value) throws IllegalStateException { ? ? ? ?Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); ? ? ? ?Assert.notNull(value, "Value must not be null"); ? ? ? ?Map<Object, Object> map = resources.get(); ? ? ? ?// set ThreadLocal Map if none found ? ? ? ?if (map == null) { ? ? ? ? ? map = new HashMap<>(); ? ? ? ? ? resources.set(map); ? ? ? ?} ? ? ? ?Object oldValue = map.put(actualKey, value); ? ? ? ?// Transparently suppress a ResourceHolder that was marked as void... ? ? ? ?if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { ? ? ? ? ? oldValue = null; ? ? ? ?} ? ? ? ?if (oldValue != null) { ? ? ? ? ? throw new IllegalStateException( ? ? ? ? ? ? ? ? "Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread"); ? ? ? ?} ? ? } ? ? // 略 }
(3) 上邊提到的openConnection方法,其實最終也是從TransactionSynchronizationManager的resources中獲取連接的
public static Connection doGetConnection(DataSource dataSource) throws SQLException { ? ? Assert.notNull(dataSource, "No DataSource specified"); ? ? // 獲取ConnectionHolder ? ? ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); ? ? if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) { ? ? ? ? logger.debug("Fetching JDBC Connection from DataSource"); ? ? ? ? Connection con = fetchConnection(dataSource); ? ? ? ? if (TransactionSynchronizationManager.isSynchronizationActive()) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ConnectionHolder holderToUse = conHolder; ? ? ? ? ? ? ? ? if (conHolder == null) { ? ? ? ? ? ? ? ? ? ? holderToUse = new ConnectionHolder(con); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? conHolder.setConnection(con); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? holderToUse.requested(); ? ? ? ? ? ? ? ? TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource)); ? ? ? ? ? ? ? ? holderToUse.setSynchronizedWithTransaction(true); ? ? ? ? ? ? ? ? if (holderToUse != conHolder) { ? ? ? ? ? ? ? ? ? ? TransactionSynchronizationManager.bindResource(dataSource, holderToUse); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } catch (RuntimeException var4) { ? ? ? ? ? ? ? ? releaseConnection(con, dataSource); ? ? ? ? ? ? ? ? throw var4; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? return con; ? ? } else { ? ? ? ? conHolder.requested(); ? ? ? ? if (!conHolder.hasConnection()) { ? ? ? ? ? ? logger.debug("Fetching resumed JDBC Connection from DataSource"); ? ? ? ? ? ? conHolder.setConnection(fetchConnection(dataSource)); ? ? ? ? } ? ? ? ? // 從ConnectionHolder中獲取連接 ? ? ? ? return conHolder.getConnection(); ? ? } }
也就是說,如果修改了數(shù)據(jù)源,那么resources中就找不到對應(yīng)的連接,就可以重新獲取連接,從而達(dá)到切換數(shù)據(jù)源的目的。然而我們數(shù)據(jù)源的只有一個,就是動態(tài)數(shù)據(jù)源,因此即使使用兩個獨立事務(wù),也不能成功切換數(shù)據(jù)源。
3. 結(jié)語
如果想要使用動態(tài)數(shù)據(jù)源的事務(wù)處理,可能需要考慮使用多線程分布式的事務(wù)處理機(jī)制;
如果使用直接注入多個數(shù)據(jù)源的方式實現(xiàn)事務(wù)處理,實現(xiàn)簡單,但是各數(shù)據(jù)源事務(wù)是獨立的;
應(yīng)該根據(jù)具體情況進(jìn)行選擇。
到此這篇關(guān)于Mybatis操作多數(shù)據(jù)源的實現(xiàn)的文章就介紹到這了,更多相關(guān)Mybatis 多數(shù)據(jù)源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MyBatisPuls多數(shù)據(jù)源操作數(shù)據(jù)源偶爾報錯問題
- Mybatis-plus配置多數(shù)據(jù)源,連接多數(shù)據(jù)庫方式
- MyBatis-Plus多數(shù)據(jù)源的示例代碼
- SpringBoot集成Mybatis實現(xiàn)對多數(shù)據(jù)源訪問原理
- Seata集成Mybatis-Plus解決多數(shù)據(jù)源事務(wù)問題
- 詳解SpringBoot Mybatis如何對接多數(shù)據(jù)源
- 一文搞懂MyBatis多數(shù)據(jù)源Starter實現(xiàn)
- Mybatis-plus多數(shù)據(jù)源配置的兩種方式總結(jié)
- MyBatis-Plus 集成動態(tài)多數(shù)據(jù)源的實現(xiàn)示例
- Mybatis-Plus的多數(shù)據(jù)源你了解嗎
- mybatis-flex實現(xiàn)多數(shù)據(jù)源操作
相關(guān)文章
Spring中的之啟動過程obtainFreshBeanFactory詳解
這篇文章主要介紹了Spring中的之啟動過程obtainFreshBeanFactory詳解,在refresh時,prepareRefresh后,馬上就調(diào)用了obtainFreshBeanFactory創(chuàng)建beanFactory以及掃描bean信息(beanDefinition),并通過BeanDefinitionRegistry注冊到容器中,需要的朋友可以參考下2024-02-02struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J(rèn)樣式的移除方法
這篇文章主要給大家介紹了關(guān)于struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J(rèn)樣式的移除方法,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10Java?C++刷題leetcode1106解析布爾表達(dá)式
這篇文章主要為大家介紹了Java?C++刷題leetcode1106解析布爾表達(dá)式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01關(guān)于java數(shù)組與字符串相互轉(zhuǎn)換的問題
這篇文章主要介紹了java數(shù)組與字符串相互轉(zhuǎn)換的問題,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10