Dynamic-load-apk插件原理解析

一、背景:

插件化的第一代目,任玉刚大神的dynamic-load-apk。目前插件化的方案主要有以Dynamic-load-apk为代表的的静态代理方案,以及以张勇的DroidPlugin为代表的动态代理hook系统AMS和PM的方案


  • 第一种方案,使用静态代理插件的方案,来代理插件apk中Activity的生命周期管理。

  • 第二种方案,使用动态代理hook系统AMS的方式,来拦截AMS启动Activity和Activity生命周期的逻辑,通过使用选坑位Activity来骗过AMS,回调到Client端再脱坑脱壳,启动targetActivity来实现。

二、解析:

这里先逐步解析一下Dynamic-load-apk的原理。

插件化涉及到几个重要的问题:

  1. 插件中资源和host宿主资源的管理(资源ID冲突为题等)
  2. 插件中四大组件的生命周期管理
  • 插件中资源的管理
    我们查看Dynamic-load-apk的工程代码,可以从DLPluginManager这个类开始。
    运行插件的第一步,肯定是加载插件apk文件。

    public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
            //标识来自外部,即从宿主调用
            mFrom = DLConstants.FROM_EXTERNAL;
            //解析插件apk的packageInfo
            PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
                    PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
            if (packageInfo == null) {
                return null;
            }
    
            //1. 准备插件运行的环境
            DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
            ///2. 如果有so文件,则需要拷贝so文件
            if (hasSoLib) {
                copySoLib(dexPath);
            }
    
            return pluginPackage;
        }
    
    

    我们先看注释1:准备插件运行环境

    
        /**
     * prepare plugin runtime env, has DexClassLoader, Resources, and so on.
     * 
     * @param packageInfo
     * @param dexPath
     * @return
     */
    private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {
        //从缓存中拿PluginPackage对象
        DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
        if (pluginPackage != null) {
            return pluginPackage;
        }
        //为当前插件创建独立的DexClassLoader
        DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
        //为当前插件创建资源管理AssetManager
        AssetManager assetManager = createAssetManager(dexPath);
        //为当前插件创建资源Resource
        Resources resources = createResources(assetManager);
        // create pluginPackage  创建新的pluginPackage对象。PluginPackage对象持有dexClassLoader、Resource、插件自己的PackageInfo
        pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
        //放进缓存
        mPackagesHolder.put(packageInfo.packageName, pluginPackage);
        return pluginPackage;
    }
        
    

    我们再分别看cerateDexClassLoader()createAssetManager()createResource()

    • createDexClassLoder(dexpath) 为当前插件创建ClassLoader。此处提一下:pathClassLoader和dexClassLoader的区别:
      1. 首先,二者都是继承自BaseDexClassLoder
      2. pathClassLoader只支持安装的apk,dexClassLoader可以支持dex、未安装的apk、aar、jar等
    private DexClassLoader createDexClassLoader(String dexPath) {
        File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
        dexOutputPath = dexOutputDir.getAbsolutePath();
        //插件所在目录,dexopt后所在目录,插件目录下放so的路径、父classloader
        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());
        return loader;
    }
    
    
    • createAssetManager(dexpath) 将插件的资源加入到宿主中
     private AssetManager createAssetManager(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            //将插件的资源一起加入到AssetManager中
            addAssetPath.invoke(assetManager, dexPath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    
    }
    
    
    
    • createResources(assetManager) 为当前插件创建资源对象
    private Resources createResources(AssetManager assetManager) {
        Resources superRes = mContext.getResources();
        Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return resources;
    }
    
  • 再看注释2:将so文件copy到插件目录下

      private void copySoLib(String dexPath) {
        // TODO: copy so lib async will lead to bugs maybe, waiting for
        // resolved later.
    
        // TODO : use wait and signal is ok ? that means when copying the
        // .so files, the main thread will enter waiting status, when the
        // copy is done, send a signal to the main thread.
        // new Thread(new CopySoRunnable(dexPath)).start();
    
        SoLibManager.getSoLoader().copyPluginSoLib(mContext, dexPath, mNativeLibDir);
    }
    
    /**
     * copy so lib to specify directory(/data/data/host_pack_name/pluginlib)
     *
     * @param dexPath      plugin path
     * @param nativeLibDir nativeLibDir
     */
    public void copyPluginSoLib(Context context, String dexPath, String nativeLibDir) {
        String cpuName = getCpuName();
        String cpuArchitect = getCpuArch(cpuName);
    
        sNativeLibDir = nativeLibDir;
        Log.d(TAG, "cpuArchitect: " + cpuArchitect);
        long start = System.currentTimeMillis();
        try {
            ZipFile zipFile = new ZipFile(dexPath);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = (ZipEntry) entries.nextElement();
                if (zipEntry.isDirectory()) {
                    continue;
                }
                String zipEntryName = zipEntry.getName();
                if (zipEntryName.endsWith(".so") && zipEntryName.contains(cpuArchitect)) {
                    final long lastModify = zipEntry.getTime();
                    if (lastModify == DLConfigs.getSoLastModifiedTime(context, zipEntryName)) {
                        // exist and no change
                        Log.d(TAG, "skip copying, the so lib is exist and not change: " + zipEntryName);
                        continue;
                    }
                    mSoExecutor.execute(new CopySoTask(context, zipFile, zipEntry, lastModify));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        long end = System.currentTimeMillis();
        Log.d(TAG, "### copy so time : " + (end - start) + " ms");
    }
    
    private class CopySoTask implements Runnable {
    
        private String mSoFileName;
        private ZipFile mZipFile;
        private ZipEntry mZipEntry;
        private Context mContext;
        private long mLastModityTime;
    
        CopySoTask(Context context, ZipFile zipFile, ZipEntry zipEntry, long lastModify) {
            mZipFile = zipFile;
            mContext = context;
            mZipEntry = zipEntry;
            mSoFileName = parseSoFileName(zipEntry.getName());
            mLastModityTime = lastModify;
        }
    
        private final String parseSoFileName(String zipEntryName) {
            return zipEntryName.substring(zipEntryName.lastIndexOf("/") + 1);
        }
    
        private void writeSoFile2LibDir() throws IOException {
            InputStream is = null;
            FileOutputStream fos = null;
            is = mZipFile.getInputStream(mZipEntry);
            fos = new FileOutputStream(new File(sNativeLibDir, mSoFileName));
            copy(is, fos);
            mZipFile.close();
        }
    
        /**
         * 输入输出流拷贝
         *
         * @param is
         * @param os
         */
        public void copy(InputStream is, OutputStream os) throws IOException {
            if (is == null || os == null)
                return;
            BufferedInputStream bis = new BufferedInputStream(is);
            BufferedOutputStream bos = new BufferedOutputStream(os);
            int size = getAvailableSize(bis);
            byte[] buf = new byte[size];
            int i = 0;
            while ((i = bis.read(buf, 0, size)) != -1) {
                bos.write(buf, 0, i);
            }
            bos.flush();
            bos.close();
            bis.close();
        }
    
        private int getAvailableSize(InputStream is) throws IOException {
            if (is == null)
                return 0;
            int available = is.available();
            return available <= 0 ? 1024 : available;
        }
    
        @Override
        public void run() {
            try {
                writeSoFile2LibDir();
                DLConfigs.setSoLastModifiedTime(mContext, mZipEntry.getName(), mLastModityTime);
                Log.d(TAG, "copy so lib success: " + mZipEntry.getName());
            } catch (IOException e) {
                Log.e(TAG, "copy so lib failed: " + e.toString());
                e.printStackTrace();
            }
    
        }
    
    }
    

    到此,整个插件的load过程全部完成,主要包括:

    1. 为插件准备运行环境
    2. copy出插件的so文件 整个过程通过loadApk加载插件会得到PluginPackager对象,
      该对象持有该插件的classLoader、该插件的资源resources、该插件的packageInfo。每个插件加载完之后,PluginPackager都会存储中缓存mPackagesHolder中,为后期启动插件做准备。
  • 启动插件(以Activity为例子)
    DlPluginManager.startPluginActivity(Context context, DLIntent dlIntent)会调用到startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode)

    /**
         * @param context
         * @param dlIntent
         * @param requestCode
         * @return One of below: {@link #START_RESULT_SUCCESS}
         *         {@link #START_RESULT_NO_PKG} {@link #START_RESULT_NO_CLASS}
         *         {@link #START_RESULT_TYPE_ERROR}
         */
        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
            //如果是插件内部调用,则不用代理中转,直接打开
            if (mFrom == DLConstants.FROM_INTERNAL) {
                dlIntent.setClassName(context, dlIntent.getPluginClass());
                performStartActivityForResult(context, dlIntent, requestCode);
                return DLPluginManager.START_RESULT_SUCCESS;
            }
    
            //如果是外部打开插件Activity,则需要一系列操作
            //1、获取插件包名
            String packageName = dlIntent.getPluginPackage();
            if (TextUtils.isEmpty(packageName)) {
                throw new NullPointerException("disallow null packageName.");
            }
    
            //根据插件包名,从缓存中获取之前加载加载解析到的DLPluginPackager对象
            DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
            if (pluginPackage == null) {
                return START_RESULT_NO_PKG;
            }
    
            //获取Activity完整路径的名称:包名+ActivityClass类名
            final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
            //用该插件自己的DexClassLoader去加载这个Activity类
            Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);
            if (clazz == null) {
                return START_RESULT_NO_CLASS;
            }
    
            // get the proxy activity class, the proxy activity will launch the
            // plugin activity.
            //根据插件的Activity去拿到插件Activity代理的Activity
            Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
            if (activityClass == null) {
                return START_RESULT_TYPE_ERROR;
            }
    
            // put extra data  往dlIntent中塞数据,给ProxyActivity使用
            dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
            dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
            dlIntent.setClass(mContext, activityClass);
            //打开Activity
            performStartActivityForResult(context, dlIntent, requestCode);
            return START_RESULT_SUCCESS;
        }
    
        public int startPluginService(final Context context, final DLIntent dlIntent) {
            if (mFrom == DLConstants.FROM_INTERNAL) {
                dlIntent.setClassName(context, dlIntent.getPluginClass());
                context.startService(dlIntent);
                return DLPluginManager.START_RESULT_SUCCESS;
            }
    
            fetchProxyServiceClass(dlIntent, new OnFetchProxyServiceClass() {
                @Override
                public void onFetch(int result, Class<? extends Service> proxyServiceClass) {
                    // TODO Auto-generated method stub
                    if (result == START_RESULT_SUCCESS) {
                        dlIntent.setClass(context, proxyServiceClass);
                        // start代理Service
                        context.startService(dlIntent);
                    }
                    mResult = result;
                }
            });
            
            return mResult;
        }
    
    

    看看如何找到代理Activity的getProxyActivityClass(clazz)

    /**
         * get the proxy activity class, the proxy activity will delegate the plugin
         * activity
         * 
         * @param clazz
         *            target activity's class
         * @return
         */
        private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {
            Class<? extends Activity> activityClass = null;
            //如果插件的Activity是继承自DLBasePluginActivity,则它在宿主host中的代理Activity是DLProxyActivity
            if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
                activityClass = DLProxyActivity.class;
                //同上
            } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
                activityClass = DLProxyFragmentActivity.class;
            }
    
            return activityClass;
        }
    

    为插件Activty找到在host中的代理Activity后,所以生命周期都交给代理Activity来实现。

    // put extra data  往dlIntent中塞数据,给ProxyActivity使用
            dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
            dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
            dlIntent.setClass(mContext, activityClass);
            //打开Activity  这里先打开activityClass,也就是host中的代理Activity
            performStartActivityForResult(context, dlIntent, requestCode);
    

    宿主host中的代理Activity有两种类型:DLProxyActivity和DLProxyFragmentActvity。
    我们拿DlProxyActivity来看,另一个同理

    /**
     * HOST中的代理Activity,所有插件的生命周期都是通过它来代理的
     */
    public class DLProxyActivity extends Activity implements DLAttachable {
    
        protected DLPlugin mRemoteActivity;
        //关键类DLPrpxyImpl是真正的实现代理类
        private DLProxyImpl impl = new DLProxyImpl(this);
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //获取塞过来的DlIntent来代理插件Activity的OnCreate
            impl.onCreate(getIntent());
        }
    
        @Override
        public void attach(DLPlugin remoteActivity, DLPluginManager pluginManager) {
            //将插件Activity与当前代理Activity绑定
            mRemoteActivity = remoteActivity;
        }
    
        //AssetManager的代理
        @Override
        public AssetManager getAssets() {
            return impl.getAssets() == null ? super.getAssets() : impl.getAssets();
        }
    
        //Resources的代理
        @Override
        public Resources getResources() {
            return impl.getResources() == null ? super.getResources() : impl.getResources();
        }
    
        //getTheme的代理
        @Override
        public Theme getTheme() {
            return impl.getTheme() == null ? super.getTheme() : impl.getTheme();
        }
    
        @Override
        public ClassLoader getClassLoader() {
            return impl.getClassLoader();
        }
    
        //以下是其他生命周期回调和其他一些回调方法的代理
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            mRemoteActivity.onActivityResult(requestCode, resultCode, data);
            super.onActivityResult(requestCode, resultCode, data);
        }
    
        @Override
        protected void onStart() {
            mRemoteActivity.onStart();
            super.onStart();
        }
    
        @Override
        protected void onRestart() {
            mRemoteActivity.onRestart();
            super.onRestart();
        }
        ...
    
    }
    
    

    关键类ProxyImpl(代理的真正实现)

    
    /**
     * This is a plugin activity proxy, the proxy will create the plugin activity
     * with reflect, and then call the plugin activity's attach、onCreate method, at
     * this time, the plugin activity is running.
     *
     * @author mrsimple
     */
    public class DLProxyImpl {
    
        private static final String TAG = "DLProxyImpl";
    
        private String mClass;
        private String mPackageName;
    
        private DLPluginPackage mPluginPackage;
        private DLPluginManager mPluginManager;
    
        private AssetManager mAssetManager;
        private Resources mResources;
        private Theme mTheme;
    
        private ActivityInfo mActivityInfo;
        private Activity mProxyActivity;
        protected DLPlugin mPluginActivity;
        public ClassLoader mPluginClassLoader;
    
        public DLProxyImpl(Activity activity) {
            mProxyActivity = activity;
        }
    
        /**
         * 初始化ActivityInfo  主要是主题
         */
        private void initializeActivityInfo() {
            PackageInfo packageInfo = mPluginPackage.packageInfo;
            if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) {
                if (mClass == null) {
                    mClass = packageInfo.activities[0].name;
                }
    
                //Finals 修复主题BUG
                int defaultTheme = packageInfo.applicationInfo.theme;
                for (ActivityInfo a : packageInfo.activities) {
                    if (a.name.equals(mClass)) {
                        mActivityInfo = a;
                        // Finals ADD 修复主题没有配置的时候插件异常
                        if (mActivityInfo.theme == 0) {
                            if (defaultTheme != 0) {
                                mActivityInfo.theme = defaultTheme;
                            } else {
                                if (Build.VERSION.SDK_INT >= 14) {
                                    mActivityInfo.theme = android.R.style.Theme_DeviceDefault;
                                } else {
                                    mActivityInfo.theme = android.R.style.Theme;
                                }
                            }
                        }
                    }
                }
    
            }
        }
    
        //为Activity设置主题
        private void handleActivityInfo() {
            Log.d(TAG, "handleActivityInfo, theme=" + mActivityInfo.theme);
            if (mActivityInfo.theme > 0) {
                mProxyActivity.setTheme(mActivityInfo.theme);
            }
            Theme superTheme = mProxyActivity.getTheme();
            mTheme = mResources.newTheme();
            mTheme.setTo(superTheme);
            // Finals适配三星以及部分加载XML出现异常BUG
            try {
                mTheme.applyStyle(mActivityInfo.theme, true);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            // TODO: handle mActivityInfo.launchMode here in the future.
        }
    
        /**
         * 最关键的方法你,外部启动插件内Activity会跑到这里
         *
         * @param intent
         */
        public void onCreate(Intent intent) {
    
            // set the extra's class loader
            intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);
    
            //获取插件包名
            mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
            //插件Activity类名
            mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
            Log.d(TAG, "mClass=" + mClass + " mPackageName=" + mPackageName);
    
            mPluginManager = DLPluginManager.getInstance(mProxyActivity);
            mPluginPackage = mPluginManager.getPackage(mPackageName);
            mAssetManager = mPluginPackage.assetManager;
            mResources = mPluginPackage.resources;
            //初始化ActivityInfo
            initializeActivityInfo();
            //为activity设置主题
            handleActivityInfo();
            //启动插件Activity
            launchTargetActivity();
        }
    
        /**
         * 启动插件内的activity:
         * 1、先反射创建Activity实例
         * 2、调用插件Activity的onCreate
         * 3、由于ProxyActivity其实是一个空Activity,所以ProxyActivity相当于一个壳子,只是负责代理回调方法
         */
        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        protected void launchTargetActivity() {
            try {
                Class<?> localClass = getClassLoader().loadClass(mClass);
                Constructor<?> localConstructor = localClass.getConstructor(new Class[]{});
                Object instance = localConstructor.newInstance(new Object[]{});
                mPluginActivity = (DLPlugin) instance;
                ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
                Log.d(TAG, "instance = " + instance);
                // attach the proxy activity and plugin package to the mPluginActivity
                mPluginActivity.attach(mProxyActivity, mPluginPackage);
    
                Bundle bundle = new Bundle();
                bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
                mPluginActivity.onCreate(bundle);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public ClassLoader getClassLoader() {
            return mPluginPackage.classLoader;
        }
    
        public AssetManager getAssets() {
            return mAssetManager;
        }
    
        public Resources getResources() {
            return mResources;
        }
    
        public Theme getTheme() {
            return mTheme;
        }
    
        public DLPlugin getRemoteActivity() {
            return mPluginActivity;
        }
    }
    

    插件中Acitivity的例子

    public class MainActivity extends DLBasePluginActivity {
    
        private static final String TAG = "MainActivity";
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            initView(savedInstanceState);
        }
    
        private void initView(Bundle savedInstanceState) {
            that.setContentView(generateContentView(that));
        }
    
        private View generateContentView(final Context context) {
            LinearLayout layout = new LinearLayout(context);
            layout.setOrientation(LinearLayout.VERTICAL);
            layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT));
            Button button = new Button(context);
            button.setText("Invoke host method");
            layout.addView(button, LayoutParams.MATCH_PARENT,
                    LayoutParams.WRAP_CONTENT);
            button.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    TestHostClass testHostClass = new TestHostClass();
                    testHostClass.testMethod(that);
                }
            });
            
            TextView textView = new TextView(context);
            textView.setText("Hello, I'm Plugin B.");
            textView.setTextSize(30);
            layout.addView(textView, LayoutParams.MATCH_PARENT,
                    LayoutParams.WRAP_CONTENT);
            return layout;
        }
    
        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            Log.d(TAG, "onActivityResult resultCode=" + resultCode);
            if (resultCode == RESULT_FIRST_USER) {
                that.finish();
            }
            super.onActivityResult(requestCode, resultCode, data);
        }
        
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            // Inflate the menu; this adds items to the action bar if it is present.
            getMenuInflater().inflate(R.menu.main, menu);
            return true;
        }
    }
    
    

    到此整个启动过程解析完成,核心是:

    1. 插件中Activity必须都继承自DLBasePluginActivity或DLBaseFragmentActvity
    2. DLProxyActivity或DlPrxoyFragment作为空壳代理了插件Activity的所有回调方法
  • Service的启动也类似。宿主也是有个代理Service叫DLProxyService,插件中的Service必须继承DLBasePluginService

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
dynamic-datasource是一个用于在Spring Boot项目中实现动态数据源的插件。它的原理是通过AOP(面向切面编程)和动态代理来实现数据源的切换。 具体来说,dynamic-datasource通过拦截数据源相关的方法,根据一定的规则来动态选择数据源。在Spring Boot项目中,我们通常会配置多个数据源,例如主数据源和从数据源。当我们需要访问不同的数据源时,可以通过在方法上添加注解来指定使用哪个数据源。 dynamic-datasource的原理可以分为以下几个步骤: 1. 定义数据源:在配置文件中配置多个数据源,并为每个数据源指定一个唯一的名称。 2. 创建数据源切换器:dynamic-datasource会根据注解中指定的数据源名称来选择对应的数据源。数据源切换器会根据当前线程的上下文来选择数据源。 3. 拦截数据源相关方法:dynamic-datasource使用AOP技术拦截数据源相关的方法,例如数据库操作的方法。 4. 根据规则选择数据源:当拦截到数据源相关的方法时,dynamic-datasource会根据一定的规则来选择数据源。例如可以根据方法名、注解等来确定使用哪个数据源。 5. 切换数据源:根据选择的数据源,dynamic-datasource会将当前线程的数据源切换为选择的数据源。 6. 执行数据库操作:在切换了数据源后,dynamic-datasource会执行数据库操作,并将结果返回给调用方。 7. 还原数据源:在数据库操作完成后,dynamic-datasource会将当前线程的数据源还原为原来的数据源。 通过以上步骤,dynamic-datasource实现了在Spring Boot项目中动态切换数据源的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值