阿里ARouter 路由框架解析

news2025/1/13 19:37:18

一、简介

众所周知,在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如果维护、扩展、解耦等成了一个非常头疼问题,随之孕育而生的诸如插件化、组件化、模块化等热门技术。 而其中组件化中一项的难点,就是实现各个组件之间的通讯,我们通常解决方案采用路由中间件,来实现页面之间跳转关系。 ARouter 是阿里开源路由框架,常被用于组件之间、模块之间的跳转,由于是国人团队开发的,所以你懂的,中文文档非常详细。

二、入门

2.1.添加依赖

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    api'com.alibaba:arouter-api:1.5.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}

2.2.初始化

在 Application 中初始化

if (BuildConfig.DEBUG) {
     ARouter.openLog()     // 打印日志
     ARouter.openDebug()   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this);

2.3.添加注解

@Route(path = "/main/main_main")
public class MainActivity extend BaseActivity {
    ...
}

2.4.发起路由

// 1.应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/main/main_main").navigation();

// 2.跳转并携带参数
ARouter.getInstance().build("/main/main_main")
            .withLong("id", 10086L)
            .withString("name", "Test")
            .navigation();

三、原理解析

在原理解析之前,我们先了解 ARouter 使用关键技术-APT(Annotation Processing Tool),它是 javac 的一个工具,中文译名为编译时注解处理器,说白了,APT 用来编译时,扫描和处理注解,获取注解和被注解类等相关信息,拿到这些信息之后,自动生成一些代码,核心是 AbstractProcessor 这个类,APT 运用非常广泛,诸如 ButterKnife、EventBus、Dagger2 等使用运用到了 APT,ARouter 也不例外。运用 APT 技术我们可以自己写一些注解处理器,例如处理网络、打印出错信息等。

3.1 分析

我们来看看 ARouter 初始化

//1.初始化
ARouter.init(application)

//2.ARouter#init
public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            //委托给_ARouter去初始化
            hasInit = _ARouter.init(application);
            if (hasInit) {
                _ARouter.afterInit();
            }
            //初始化之后调用afterInit
            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
    }

从上面我们可以看到 ARouter 采用门面模式,实际上委托给 *ARouter 处理,*ARouter 是整个框架的路由中心控制器,负责处理控制整个路由的流程。接下来,我们看看 _ARouter 初始化化。

protected static synchronized boolean init(Application application) {
        mContext = application;
        //调用 LogisticsCenter 的初始化
        LogisticsCenter.init(mContext, executor);
        logger.info(Consts.TAG, "ARouter init success!");
        hasInit = true;
        mHandler = new Handler(Looper.getMainLooper());
        return true;
}

//LogisticsCenter#init
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;
        try {
            long startInit = System.currentTimeMillis();
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else {
                Set<String> routerMap;
                //1.如果是debug模式,或者有更新
                if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                    logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
       
                    //1.扫描包,获取ROUTE_ROOT_PAKCAGE(com.alibaba.android.arouter.routes)包下的类
                    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                    if (!routerMap.isEmpty()) {
                        context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                    }
                    PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
                } else {
                    
                    logger.info(TAG, "Load router map from cache.");
                    //2.获取本地缓存
                    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
                }

                logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
                startInit = System.currentTimeMillis();
                for (String className : routerMap) {
                    //3.将 IRouteRoot,IRouteGroup和IProviderGroup的实现类,
                    //通过注解生成,并且加载 Warehouse(数据仓库)
                    // Warehouse:数据仓库,存储路由配置信息和具体生成的IProvider对象
                    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                        //1.加载 IRouteRoot,每个moudle下都会生成一个该类型的实现类,
                        //通过module名来区分的,作用是将每个module下所有的分组按照组名
                        //和对应分组的实现类(IRouteGroup接口的实现类)的 Class 对象做一个映射,
                        //然后保存在一个全局的 groupIndex 的 map 表中。
                        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                        //2.加载 IInterceptorGroup 作用是将各个module下的  自定义的i nterceptor 按照优先级和 interceptor的       
                        // Class 对象做一个映射,然后保存在一个全局的 interceptorIndex 的 map 表中。
                        ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                         //3.加载 IProviderGroup 该类的作用的是将项目中自定义的提供序
                         //列化功能的类的相关信息以RouteMeta 类的对象保存在全局的providerIndex的map表中
                        ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                    }
                }
            }

            logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
    }

从上面代码我们可以看到调用 LogisticsCenter,后勤中心主要完成两件事

  • 获取到 com.alibaba.android.arouter.routes 包下的所有 class 文件类名

  • 根据找到的类名去加载相关的实例到Warehouse中,com.alibaba.android.arouter.routes 包下面的 class 是注解解析器自动生成,主要 IRouteRoot,IRouteGroup 和 IProviderGroup 的实现类,当我们使用 @Route 注解某个类时,会将这个类的信息注入的到自动生成的上述实现类中。 接下来调用 afterInit()。

//_ARouter#afterInit
static void afterInit() {
        // 调用 Postcard#build 方法,获取 InterceptorService 拦截服务控制器
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }
    
 //build 方法实际上,将 path 和 group 信息封装到 Postcard,生成一个要跳转的信息表
 protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            //生成具体实例
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path));
        }
    }

//Postcard#navigation 这个方法实现了跳转,我们点进去会发现,最后调用 _ARouter#navigation
//_ARouter#navigation
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
        if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {
            // Pretreatment failed, navigation canceled.
            return null;
        }

        try {
            //1.调用 LogisticsCenter#completion 的方法
            LogisticsCenter.completion(postcard);
             ......
         //不是绿色通道,通过拦截控制器依次调用不同拦截器处理信息  
        if (!postcard.isGreenChannel()) {  
            //每个拦截器的拦截方法调用都是在子线程中执行的
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        //只要有一个拦截器拦截该包裹,则回调onInterrupt方法宣告本次路由被终止
                        callback.onInterrupt(postcard);
                    }
                }
            });
        } else {
            //2.如果是绿色通道,调用_navigation方法进行具体的导航
            return _navigation(context, postcard, requestCode, callback);
        }
        return null;
    }   
 }
 
//LogisticsCenter#completion
public synchronized static void completion(Postcard postcard) {
     ......
     //1.Warehouse.routes获取RouteMeta,RouteMeta 路由信息描述类,存储目标地址的类型,路径,参数等信
     RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
     if (null == routeMeta) {   
            //如果没有路由信息,则尝试去数据仓库查找
           Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  
           if (null == groupMeta) {
               throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
          } else {
                // Load route and cache it into memory, then delete from metas.
              try {
                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }

                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());

                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }

                completion(postcard);   // Reload
            }  else {
             //找到路由信息后,则将配置的路由信息填充到Postcard对象中
              postcard.setDestination(routeMeta.getDestination());//要跳转 Activity.class 路径
              postcard.setType(routeMeta.getType());
              postcard.setPriority(routeMeta.getPriority());
              postcard.setExtra(routeMeta.getExtra());
                  Uri rawUri = postcard.getUri();
            if (null != rawUri) {   
                //这里主要是完成参数的填充
            }
            //针对不同的路由类型进行处理
            switch (routeMeta.getType()) {
                case PROVIDER:  
                    //如果是服务提供者,则尝试获取其具体实例,如果没有,则根据路由信息构造一个实例,初始化并存储到数据仓库,
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                    }
                    postcard.setProvider(instance);
                    //服务提供者被设置成绿色渠道,不用接受拦截检查
                    postcard.greenChannel();   
                    break;
                case FRAGMENT:
                   //fragment也不用拦截检查
                    postcard.greenChannel();  
                default:
                    break;
           }
    }

//_ARouter#_navigation
//根据不同类型,路由处理和导航也不一样
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
           //是 ACTIVITY 数据填充到intent,并且调用 startActivity
            case ACTIVITY:
                
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Set Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                     
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });

                break;
            //PROVIDER类型,则直接返回其服务提供者
            case PROVIDER:
                return postcard.getProvider();
             //如果是BOARDCAST 、 CONTENT_PROVIDER 、 FRAGMENT,则创建其需要的实体,并填充数据,返回对象   
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

上面的初始化代码其实就可以了解其原理,下面我们可以画出时序图,了解大致流程。

四、总结

ARouter 通过 apt 技术,生成保存路径(路由path)被注解(@Router)的组件类的映射关系的类,利用这些保存了映射关系的类,根据用户的请求 postcard(明信片)寻找到要跳转的目标地址(class),使用 Intent 跳转。

ARouter的重要性主要体现在以下几个方面:

  1. 模块化开发:随着软件规模和复杂度的增加,越来越多的应用程序采用模块化开发方式。ARouter支持跨模块的调用,简化了模块间跳转的复杂度,降低了耦合性,提升了整个应用的可维护性和可扩展性。
  2. 代码解耦:在传统的Activity跳转方式中,一般是通过Intent来传递参数,不同页面之间的参数传递比较麻烦,还有可能导致代码冗余。ARouter采用注解方式传递参数,简化了代码的编写和阅读。
  3. 动态路由:路由是基于注解方式实现的,可以在代码中动态注册和删除路由。ARouter还支持路由重定向、降级等高级功能,可以根据不同的业务场景灵活处理路由逻辑。
  4. 统一管理:ARouter提供了统一管理路由配置的界面,可以直览整个项目的路由规则,方便开发人员进行维护和管理。

ARouter是一款非常实用的路由框架,可以帮助开发者在模块化开发、代码解耦、动态路由和统一管理等方面提高开发效率和应用质量。

下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接 通过点击此处↓↓↓ 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

Android 架构学习手册

深入理解Gradle框架

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/435870.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud 之五:Feign使用Hystrix

系列目录&#xff08;持续更新。。。&#xff09; Spring Cloud之一&#xff1a;注册与发现-Eureka工程的创建 Spring Cloud之二&#xff1a;服务提供者注册到Eureka Server Spring Cloud之三&#xff1a;Eureka Server添加认证 Spring Cloud之四&#xff1a;使用Feign实现…

camunda如何监控流程执行

在 Camunda 中&#xff0c;可以使用 Camunda 提供的用户界面和 API 来监控流程的执行情况。以下是几种常用的监控流程执行的方式&#xff1a; 1、使用 Camunda Cockpit&#xff1a;Camunda Cockpit 是 Camunda 官方提供的流程监控和管理工具&#xff0c;可以在浏览器中访问 Co…

【百面成神】消息中间件基础7问,你能撑到第几问

前 言 &#x1f349; 作者简介&#xff1a;半旧518&#xff0c;长跑型选手&#xff0c;立志坚持写10年博客&#xff0c;专注于java后端 ☕专栏简介&#xff1a;纯手打总结面试题&#xff0c;自用备用 &#x1f330; 文章简介&#xff1a;消息中间件最基础、重要的9道面试题 文章…

Android中的MVVM架构:使用Jetpack组件实现现代化的应用架构

Android中的MVVM架构&#xff1a;使用Jetpack组件实现现代化的应用架构 Jetpack组件是构建现代Android应用的绝佳利器&#xff0c;组件化设计让构建App如此简单。 引言 随着移动应用的日益复杂和功能的不断增加&#xff0c;构建稳健、可扩展和易维护的Android应用变得越来越重…

[考研数据结构] 第3章之队列的基本知识与操作

文章目录 队列的基本概念 队列的顺序存储 顺序队列 存储类型 基本操作 循序队列 存储类型 基本操作 循环队列判空与判满的三种解决方案 方法一&#xff1a;牺牲一个存储单元 方法二&#xff1a;类型增设记录型变量size 方法三&#xff1a;类型增设标志型变量tag 队…

嵌入式【协议篇】CAN协议原理

一、CAN协议介绍 1、简介 CAN是控制器局域网络(Controller Area Network, CAN)的简称,是一种能够实现分布式实时控制的串行通信网络。 其实可以简单把CAN通信理解成开一场电话会议,当一个人讲话时其他人就听(广播),当多个人同时讲话时则根据一定规则来决定谁先讲话谁后讲…

【音视频】 zlm的几个代理接口解释

目录 12、/index/api/addStreamProxy 30、/index/api/addStreamPusherProxy 14、/index/api/addFFmpegSource 24、/index/api/openRtpServer 27、/index/api/startSendRtp 参考 12、/index/api/addStreamProxy 拉流代理 &#xff1a; 194上在播放。 而10.30.2.6上加上这个…

FastDGCNN

Faster Dynamic Graph CNN: Faster Deep Learning on 3D Point Cloud Data | IEEE Journals & Magazine | IEEE Xplore ​​​​​​​题目&#xff1a;Faster Dynamic Graph CNN: Faster Deep Learning on 3D Point Cloud Data&#xff08;更快的动态图形CNN&#xff1a;对…

Android 对View 进行旋转、缩放、平移的属性变换后,获取外矩形顶点

文章目录 前言改变 View 的属性&#xff0c;进行旋转、缩放、平移输出 View 的属性 使用 matrix 映射 view 变换后的外矩形前(左)乘(preXxx)、后(右)乘(postXxx) 对映射结果的影响前(左)乘(preXxx) 的意义后(右)乘(postXxx) 结论 来张图 前言 Android View 通过平移、旋转、…

找PPT模板就上这5个网站~

分享几个可以永久免费下载PPT模板、素材的网站&#xff0c;上万个模板随便下载&#xff0c;赶紧收藏起来~ 1、菜鸟图库 https://www.sucai999.com/search/ppt/0_0_0_1.html?vNTYxMjky 网站素材非常全面&#xff0c;主要以设计类素材为主&#xff0c;办公类素材也很多&#x…

4、RSA终端指令

RSA总结 加密算法,都是数学知识对称加密(传统加密算法)RSA(三个人的名字)非对称加密(现代加密算法) 原根欧拉函数、欧拉定理(费马小定理)模反元素 m^(e * d) mod n ≡ m迪菲赫尔曼密钥交换RSA算法 RSA: 拆解两个(大)质数的乘积很难!所以RSA想对安全.加密: M ^e % N C解密: C…

前端学习:HTML头部、布局

目录 HTML头部 一、HTML 元素 二、head标签和header标签的不同 三、HTML 元素 四、HTML 元素 五、HTML 元素 六、 HTML 七、HTML元素 为搜索引擎定义关键词&#xff1a; 为网页定义描述内容&#xff1a; 每60秒刷新当前页面&#xff1a; 八、HTML 九、HTML头部元素…

Vue项目搭建流程

目录 1、通过命令创建 2、npm下载依赖 3、路由配置 4、配置组件 5、对axios进行二次封装 6、全局接口请求封装 7、配置跨域(反向代理) 1、通过命令创建 create vue 项目名 2、npm下载依赖 nmp i 依赖名版本号 axios1.2.1 echarts5.1.2 element-ui2.15.12 vue-router3…

56 openEuler搭建Mariadb数据库服务器-安装、运行和卸载

文章目录 56 openEuler搭建Mariadb数据库服务器-安装、运行和卸载56.1 安装56.2 运行56.3 卸载 56 openEuler搭建Mariadb数据库服务器-安装、运行和卸载 56.1 安装 配置本地yum源&#xff0c;详细信息请参考《openEuler 22.03-LTS 搭建repo服务器》。 清除缓存。 # dnf clean…

【SpringBoot】1、SpringBoot整合JWT实现Token验证

这里写目录标题 1.单点登录1.1 单系统登录1.1.1 单系统登录流程(使用Session实现单系统登录) 1.2 多系统(单点)登录1.2.1 单点登录实现方案1.2.1.1 Session跨域1.2.1.2 Spring Session共享 1.3 Token机制1.3.1 传统身份认证1.3.2 基于Token的身份认证 1.4 JWT机制1.4.1 JWT数据…

Redis集群部署详解

文章目录 集群环境集群搭建测试集群故障转移集群扩容集群缩容 集群环境 集群介绍 1.什么是集群 所谓的集群&#xff0c;就是通过增加服务器的数量&#xff0c;提供相同的服务&#xff0c;从而让服务器达到一个稳定、高效的状态。 2.使用redis集群的必要性 单个redis存在不稳定…

9.6 数组的指针和指向数组的指针变量 - 3

9.6 数组的指针和指向数组的指针变量 - 3 一.回顾二维数组和多维数组的概念二.指向多维数组的指针和指针变量探究1.a:二维数组名&#xff0c;也是整个二维数组的首地址。我们可以认为是第0行的首地址是10002.a1 , a2 分别代表第一行首地址和第二行首地址。3.这表示a[0],a[1],a[…

Mapbox-gl.js v2.13.0 扩展支持4326,4490坐标系

mapbox-gl.js新版本中&#xff0c;支持多种projection 显示效果也不错&#xff0c;根据tiles grid可以看到&#xff0c;还是web_mercator的格网&#xff0c;基于图片做了一定的拉伸形变&#xff0c;想要加载4326的切片格网&#xff0c;依然无法实现。 后来在网上搜索加载4326切…

【JavaWeb】后端(Maven+SpringBoot+HTTP+Tomcat)

目录 一、Maven1.什么是Maven?2.Maven的作用?3.介绍4.安装5.IDEA集成Maven6.IDEA创建Maven项目7.IDEA导入Maven项目8.依赖配置9.依赖传递10.依赖范围11.生命周期 二、SpringBoot1.Spring2.SpringBoot3.SpringBootWeb快速入门 二、HTTP1.HTTP-概述2.HTTP-请求协议3.HTTP-响应协…

【转行互联网】转行互联网必看答疑

课程 追忆寻梦-转行互联网必备知识 https://edu.csdn.net/course/detail/31180 2023年&#xff0c;迟来的编程私教服务 https://bbs.csdn.net/topics/613231237 优先 必读文章 初学者&#xff0c;打算改行学编程&#xff0c;怎么学习java&#xff1f;求指教。https://bb…