业务场景
我们项目有dev和prod两个运行时环境,dev、prod在AWS上的账号是分开的,服务端的API也是分开的,也就是两个环境。现在的需求是,希望在App运行时能动态的实现两个运行环境的互换。
实现过程
1、服务端的API动态切换,这个很容易就能实现,根据环境标识使用相应的一套API就可以。(不同的地方,比如baseUrl、baserLbsUrl、各个第三方平台的key等)
2、切换AWS的初始化环境,因为不同的环境是根据配置的信息去初始化的,也就是需要重置初始化环境。
遇到的问题
1、AWS SDK中提供的初始化API,强制限制只能初始化一次。使用的是如下API
try {
val amplifyConfig = if (BuildEnvConfig.isProdEnv()) {
R.raw.amplifyconfiguration_prod
} else {
R.raw.amplifyconfiguration
}
val config = AmplifyConfiguration.builder(application, amplifyConfig)
.devMenuEnabled(false)
.build()
Amplify.configure(config, application)
} catch (e: AmplifyException) {
e.printStackTrace()
}
初始化配置Amplify.configure(config, application)
进入方法内部,如下截图
发现它内部有一个配置同步锁,在Amplify类中的定义如下
private static final AtomicBoolean CONFIGURATION_LOCK = new AtomicBoolean(false);
一旦初始化后,若再次初始化,则抛出如下异常
throw new AmplifyException(
"The client issued a subsequent call to `Amplify.configure` after the first had already succeeded.", "Be sure to only call Amplify.configure once"
);
CONFIGURATION_LOCK
是私有的静态常量,也就是说我们改不了
2、使用Java的反射技术(代码是使用的Kotlin语言),去尝试修改,代码如下:
val amplifyClass: Class<*> = Amplify::class.java
val configurationLock = amplifyClass.getDeclaredField("CONFIGURATION_LOCK")
configurationLock.isAccessible = true
configurationLock.set(null, AtomicBoolean(false))
编译运行,发现还是不行,遇到了如下异常(是UserAgent类的)
throw new AmplifyException(
"User-Agent was already configured successfully.",
"User-Agent is configured internally during Amplify configuration. " +
"This method should not be called externally."
);
通过翻阅源码,发现UserAgent类有提供重置方法
UserAgent.reset()
我们再次编译运行,发现还是不行,遇到了如下异常(是Category类的)
if (!State.NOT_CONFIGURED.equals(state.get())) {
throw new AmplifyException(
"Category " + getCategoryType() + " has already been configured or is currently configuring.", "Ensure that you are only attempting configuration once."
);
}
翻阅Category类的源码,看到了如下代码
/**
* Records the initialization state. See {@link State} for possible values.
*/
private final AtomicReference<State> state;
/**
* Constructs a new, not-yet-configured, Category.
*/
public Category() {
this.plugins = new ConcurrentHashMap<>();
this.state = new AtomicReference<>(State.NOT_CONFIGURED);
}
也就是state变量的值,我们通过常规的手段修改不了,我们继续使用反射技术,代码如下:
try {
val categoryClass = Class.forName("com.amplifyframework.core.category.Category\$State")
val notConfiguredField = categoryClass.getField("NOT_CONFIGURED")
notConfiguredField.isAccessible = true
val notConfigured = notConfiguredField.get(null)
val authCategoryState = Class.forName("com.amplifyframework.auth.AuthCategory").superclass.getDeclaredField("state")
authCategoryState.isAccessible = true
authCategoryState.set(Amplify.Auth, AtomicReference(notConfigured))
val analyticsState = Class.forName("com.amplifyframework.analytics.AnalyticsCategory").superclass.getDeclaredField("state")
analyticsState.isAccessible = true
analyticsState.set(Amplify.Analytics, AtomicReference(notConfigured))
val apiState = Class.forName("com.amplifyframework.api.ApiCategory").superclass.getDeclaredField("state")
apiState.isAccessible = true
apiState.set(Amplify.API, AtomicReference(notConfigured))
val loggingState = Class.forName("com.amplifyframework.logging.LoggingCategory").superclass.getDeclaredField("state")
loggingState.isAccessible = true
loggingState.set(Amplify.Logging, AtomicReference(notConfigured))
val storageState = Class.forName("com.amplifyframework.storage.StorageCategory").superclass.getDeclaredField("state")
storageState.isAccessible = true
storageState.set(Amplify.Storage, AtomicReference(notConfigured))
val hubState = Class.forName("com.amplifyframework.hub.HubCategory").superclass.getDeclaredField("state")
hubState.isAccessible = true
hubState.set(Amplify.Hub, AtomicReference(notConfigured))
val dataStoreState = Class.forName("com.amplifyframework.datastore.DataStoreCategory").superclass.getDeclaredField("state")
dataStoreState.isAccessible = true
dataStoreState.set(Amplify.DataStore, AtomicReference(notConfigured))
val predictionsState = Class.forName("com.amplifyframework.predictions.PredictionsCategory").superclass.getDeclaredField("state")
predictionsState.isAccessible = true
predictionsState.set(Amplify.Predictions, AtomicReference(notConfigured))
val categoriesMap = buildCategoriesMap()
val categories = amplifyClass.getDeclaredField("CATEGORIES")
categories.isAccessible = true
categories.set(null, categoriesMap)
val initializationPool = amplifyClass.getDeclaredField("INITIALIZATION_POOL")
initializationPool.isAccessible = true
initializationPool.set(null, Executors.newFixedThreadPool(categoriesMap.size))
} catch (e: Exception) {
e.printStackTrace()
}
编译、运行,这次发现终于不再包异常了,那问题解决了吗?实现了我们在运行时,动态切换AWS账号及环境的目的了没?答案是:没有!。 你没看错,现在呢是编译、运行一切正常,但是就是没实现运行时动态切AWS账号及环境。
关于重置AWS SDK 初始化相关限制的完整代码如下:
package com.majiapp.app
import android.annotation.SuppressLint
import android.app.Application
import com.amplifyframework.AmplifyException
import com.amplifyframework.core.Amplify
import com.amplifyframework.core.AmplifyConfiguration
import com.amplifyframework.core.category.Category
import com.amplifyframework.core.category.CategoryType
import com.amplifyframework.core.plugin.Plugin
import com.amplifyframework.util.UserAgent
import com.majiapp.R
import com.majiapp.buildconfig.BuildEnvConfig
import com.majiapp.tu.TuApplication
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
object AppEvnInit {
fun init(appContext: TuApplication) {
resetCategoriesState()
initAmplify(appContext)
}
private fun initAmplify(application: Application) {
try {
val amplifyConfig = if (BuildEnvConfig.isProdEnv()) {
R.raw.amplifyconfiguration_prod
} else {
R.raw.amplifyconfiguration
}
val config = AmplifyConfiguration.builder(application, amplifyConfig)
.devMenuEnabled(false)
.build()
Amplify.configure(config, application)
} catch (e: AmplifyException) {
e.printStackTrace()
}
}
@SuppressLint("VisibleForTests")
private fun resetCategoriesState() {
try {
UserAgent.reset()
val categoryClass = Class.forName("com.amplifyframework.core.category.Category\$State")
val notConfiguredField = categoryClass.getField("NOT_CONFIGURED")
notConfiguredField.isAccessible = true
val notConfigured = notConfiguredField.get(null)
val authCategoryState = Class.forName("com.amplifyframework.auth.AuthCategory").superclass.getDeclaredField("state")
authCategoryState.isAccessible = true
authCategoryState.set(Amplify.Auth, AtomicReference(notConfigured))
val analyticsState = Class.forName("com.amplifyframework.analytics.AnalyticsCategory").superclass.getDeclaredField("state")
analyticsState.isAccessible = true
analyticsState.set(Amplify.Analytics, AtomicReference(notConfigured))
val apiState = Class.forName("com.amplifyframework.api.ApiCategory").superclass.getDeclaredField("state")
apiState.isAccessible = true
apiState.set(Amplify.API, AtomicReference(notConfigured))
val loggingState = Class.forName("com.amplifyframework.logging.LoggingCategory").superclass.getDeclaredField("state")
loggingState.isAccessible = true
loggingState.set(Amplify.Logging, AtomicReference(notConfigured))
val storageState = Class.forName("com.amplifyframework.storage.StorageCategory").superclass.getDeclaredField("state")
storageState.isAccessible = true
storageState.set(Amplify.Storage, AtomicReference(notConfigured))
val hubState = Class.forName("com.amplifyframework.hub.HubCategory").superclass.getDeclaredField("state")
hubState.isAccessible = true
hubState.set(Amplify.Hub, AtomicReference(notConfigured))
val dataStoreState = Class.forName("com.amplifyframework.datastore.DataStoreCategory").superclass.getDeclaredField("state")
dataStoreState.isAccessible = true
dataStoreState.set(Amplify.DataStore, AtomicReference(notConfigured))
val predictionsState = Class.forName("com.amplifyframework.predictions.PredictionsCategory").superclass.getDeclaredField("state")
predictionsState.isAccessible = true
predictionsState.set(Amplify.Predictions, AtomicReference(notConfigured))
val amplifyClass: Class<*> = Amplify::class.java
val configurationLock = amplifyClass.getDeclaredField("CONFIGURATION_LOCK")
configurationLock.isAccessible = true
configurationLock.set(null, AtomicBoolean(false))
val categoriesMap = buildCategoriesMap()
val categories = amplifyClass.getDeclaredField("CATEGORIES")
categories.isAccessible = true
categories.set(null, categoriesMap)
val initializationPool = amplifyClass.getDeclaredField("INITIALIZATION_POOL")
initializationPool.isAccessible = true
initializationPool.set(null, Executors.newFixedThreadPool(categoriesMap.size))
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun buildCategoriesMap(): LinkedHashMap<CategoryType, Category<out Plugin<*>?>> {
val categories = LinkedHashMap<CategoryType, Category<out Plugin<*>?>>()
categories[CategoryType.AUTH] = Amplify.Auth // This must be before ANALYTICS, API, STORAGE, & PREDICTIONS
categories[CategoryType.ANALYTICS] = Amplify.Analytics
categories[CategoryType.API] = Amplify.API
categories[CategoryType.LOGGING] = Amplify.Logging
categories[CategoryType.STORAGE] = Amplify.Storage
categories[CategoryType.HUB] = Amplify.Hub
categories[CategoryType.DATASTORE] = Amplify.DataStore
categories[CategoryType.PREDICTIONS] = Amplify.Predictions
return categories
}
}
折中的解决办法
在切换环境时,记录下一次运行时(存入SharedPreferences中),想要运行的环境,之后kill App进程,点击App Icon重新打开App。(这个功能是给公司内部自己人用的)