ARouter路由框架

news2024/11/25 5:03:58

序、时间于同样的方式留经我们每个人,每个人却以不同的方式对待时间。

前言

一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

1.原理解析

1.1初始化

使用ARouter在进行Activity跳转非常简单:初始化ARouter、添加注解@Route、发起路由。

初始化SDK

// 在module app中
//1.初始化SDK
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化
// moduleA
// 2.在支持路由的页面上添加注解(必选)
// 路径注意至少需有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
// moduleB(没有依赖 moduleA)
// 3.发起路由跳转
ARouter.getInstance().build("/test/activity").navigation();

这样就使得 没有依赖moduleA的moduleB能跳转到moduleA的Activity了。服务获取也是类似简单的代码就可实现。

疑问:如何通过没有依赖完成 解耦组件间的路由操作呢?

1.2构建构建PostCard

先看ARouter.getInstance():

public static ARouter getInstance() {
    if (!hasInit) { // 未初始化则报异常
        throw new InitException("ARouter::Init::Invoke init(context) first!");
    } else {
        if (instance == null) {
            synchronized (ARouter.class) {
                if (instance == null) {
                    instance = new ARouter();
                }
            }
        }
        return instance;
    }
}

获取ARouter单实例,没有初始化则报异常。再看它的build(string)方法:

public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

这里调用的是_ARouter的同名方法,返回的是(Postcard)。

protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) { //path不能为空
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        //path替换,这是预处理
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path), true);
    }
}

其中参数group是通过extractGroup(path)获取,也就是path的第一级,即"/test/activity"中的"test"。group的作用是作为路由的默认分组。

路由中的分组概念:

  • SDK中针对所有的路径(/test/1、/test/2)进行分组,分组只有在分组中的某一个路径第一次被访问的时候,该分组才会被初始化
  • 可以通过 @Route 注解主动指定分组,否则使用路径中第一段字符串(/*/)作为分组
  • 注意:一旦主动指定分组之后,应用内路由需要使用 ARouter.getInstance().build(path, group) 进行跳转,手动指定分组,否则无法找到 @Route(path = "/test/1", group = "app")

1.3路由过程

通过path构建了PostCard后调用了其navigation()方法,也就是开始了路由过程:

public Object navigation() {
    return navigation(null);
}
public Object navigation(Context context) {
    return navigation(context, null);
}
public Object navigation(Context context, NavigationCallback callback) {
    return ARouter.getInstance().navigation(context, this, -1, callback);
}

看到连续走了两个重载方法,最后走到ARouter的navigation方法,并且把自己传了进去。ARouter的navigation方法同样会走到_ARouter的同名方法:

// @param context     Activity or null.
 // @param postcard    Route metas
 // @param requestCode RequestCode,startActivity的requestCode
 // @param callback    cb,路由回调:找到目的地、未找到、中断、到达
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    //若有PretreatmentService的实现,就进行预处理。可以在真正路由前进行一些判断然后中断路由。
    PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
    if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {
        // 预处理失返回false,路由取消.
        return null;
    }

    // 给路由设置context,例如启动activity需要。如果没有传就使用mContext,即初始化ARouter时传入的Application
    postcard.setContext(null == context ? mContext : context);

    try {
        // 1. 完善postcard信息(目前只有path、group,还需要知道具体目的地,例如要跳转到的activity信息)
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        //这里就是debug包中,没找到路由目的地时 经常出现的提示
        if (debuggable()) {
            runInMainThread(new Runnable() {
                public void run() {
                    Toast.makeText(mContext, "There's no route matched!\n" +" Path = [" + postcard.getPath() + "]\n" +
                            " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
                }
            });
        }
        //没找到的回调
        if (null != callback) {
            callback.onLost(postcard);
        } else {
            // 没有callback的话, (如果有)就回调到降低服务 
            DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
            if (null != degradeService) {
                degradeService.onLost(context, postcard);
            }
        }
        return null;
    }
    //找到的回调
    if (null != callback) {
        callback.onFound(postcard);
    }
    // 2. 不是绿色通道的话,要先走拦截器
    if (!postcard.isGreenChannel()) {
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            
            //拦截器处理结果:继续路由
            @Override
            public void onContinue(Postcard postcard) {
                // 3. 获取路由结果
                _navigation(postcard, requestCode, callback);
            }

             //拦截器处理结果:中断路由,回调中断
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }
            }
        });
    } else {
        //绿色通道,不走拦截器,就获取路由结果
        return _navigation(postcard, requestCode, callback);
    }

    return null;
}

使用前面构建好的PastCard经过整体3个步骤,就完成了路由:

  1. 完善postcard信息:只有path、group还不行,还需要知道具体目的地,例如要跳转到的Activity信息。
  2. 拦截器处理:不是绿色通道的话,要先经过路由器的处理,结果是中断或者继续。
  3. 获取路由结果:例如进行目标Activity的跳转、获取IProvider实现对象、获取Fragment等。

1.4获取路由结果

private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    final Context currentContext = postcard.getContext();
    //根据路由类型来走对应逻辑
    switch (postcard.getType()) {
        case ACTIVITY:
            // Activity, 使用postcard.getDestination()来构建intent、传入Extras、设置 flags、action
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            intent.putExtras(postcard.getExtras());
            int flags = postcard.getFlags();
            if (0 != flags) {
                intent.setFlags(flags);
            }
            // 当前的context不是activity,需要添加flag:FLAG_ACTIVITY_NEW_TASK
            //(启动startActivity需有任务栈的,application/service启动activity没有任务栈,所以必须要new_task_flag新建一个任务栈)
            if (!(currentContext instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

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

            // 最后在主线程执行 熟悉的startActivity,
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    startActivity(requestCode, currentContext, intent, postcard, callback);
                }
            });
            break;
        case PROVIDER:
            //provider,指的是想要获取的服务,即IProvider的实现类。直接从postCard中获取。
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
        //Broadcast、ContentProvider、Fragment,都是使用postcard.getDestination()反射创建实例
            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;
            } ...
    }
    return null;
}

从上面可见,postcard 经过完善后,路由类型type、目的地destination等都已经被赋了值。destination就是目标类的class对象。

方法内容就是根据路由类型来走对应逻辑:

  • Activity, 使用postcard.getDestination()来构建intent、传入Extras、设置 flags、action,最后在主线程执行 熟悉的startActivity
  • provider,指的是想要获取的服务,即IProvider的实现类。是直接从postCard中获取的,因为服务类是单例,只会在首次获取时创建()。
  • Broadcast、ContentProvider、Fragment,都是使用postcard.getDestination()反射创建实例

整体逻辑还是比较简单的。这里你可能好奇 destination的值是如何获取的,因为无论哪种类型的路由,都是要使用目标class,这个就是ARouter最为核心的内容——如何获取 无直接依赖的模块的 class对象,也就是完善postcard信息的过程。 不过我们先来把拦截器逻辑分析完,最后再来看这个核心点。

1.5拦截器

未设置绿色通道的路由需要经过拦截器处理,也就是interceptorService的doInterceptions()方法。interceptorService是啥呢?

final class ARouter {
    ...
    private static InterceptorService interceptorService;
    ...
    //ARouter的初始化方法
    public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            hasInit = _ARouter.init(application);
            if (hasInit) {
                _ARouter.afterInit();
            }
        }
    }
   ...
}

  //_ARouter.java
    static void afterInit() {
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }

InterceptorService继承IProvider,可见interceptorService也是一个服务,在ARouter初始化后 获取,用于处理拦截器的逻辑。具体的实现类是InterceptorServiceImpl:

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService {
...
    @Override
    public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
        //有拦截器
        if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
            ...
            LogisticsCenter.executor.execute(new Runnable() { //放入线程池异步执行
                @Override
                public void run() { //interceptorCounter 用于保证所有拦截器都走完,并且设置了超时
                    CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
                    try {//执行第一个拦截器,如果没有中断 则递归调用继续后面的拦截器
                        _execute(0, interceptorCounter, postcard);
                        interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS);
                        if (interceptorCounter.getCount() > 0) {    // count>0说明超时了,拦截器还没处理完.
                            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
                        } else if (null != postcard.getTag()) {    //Tag!=null说明被某个拦截器回调中断了
                            callback.onInterrupt((Throwable) postcard.getTag());
                        } else {
                            callback.onContinue(postcard); // 所有拦截器处理完、没超时、也没异常,则继续路由
                        }
                    }...
                }
            });
        } else {
            //没有拦截器则继续路由
            callback.onContinue(postcard);
        }
    }

    private static void _execute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
        if (index < Warehouse.interceptors.size()) {
            //从Warehouse.interceptors中获取第index个拦截器,走process方法,如果回调到onContinue就继续下一个;
            IInterceptor iInterceptor = Warehouse.interceptors.get(index);
            iInterceptor.process(postcard, new InterceptorCallback() {
                @Override
                public void onContinue(Postcard postcard) {
                    counter.countDown();
                    _execute(index + 1, counter, postcard);  // 继续下一个
                }
                @Override
                public void onInterrupt(Throwable exception) {
                    postcard.setTag(null == exception ? new HandlerException("No message.") : exception);    // save the exception message for backup.
                    counter.cancel();
                    ...
                }
            });
        }
    }
    
    @Override //此init方法会在服务被创建后调用。这里就是反射创建所有的拦截器实例
    public void init(final Context context) {
        LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run() {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                    //遍历Warehouse.interceptorsIndex ,使用存储与其中的拦截器class对象反射创建拦截器实例
                    for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                        Class<? extends IInterceptor> interceptorClass = entry.getValue();
                        try {
                            IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                            iInterceptor.init(context);
                            //存入 Warehouse.interceptors
                            Warehouse.interceptors.add(iInterceptor);
                        }...
                    }
                    interceptorHasInit = true;
                    ...                }
            }
        });
    }...
}

doInterceptions()方法中判断如果有拦截器,就放入线程池异步执行第一个拦截器,且使用interceptorCounter 保证所有拦截器都走完,同时也设置了超时。 如果第一个拦截器没有回调中断 则递归调用继续后面的拦截器。

拦截器的执行,是从Warehouse.interceptors中获取第index个拦截器,走process方法,如果回调到onContinue就继续下一个;若回调onInterrupt就中断路由。

拦截器的执行逻辑还是比较清晰的。那么拦截器是怎么获取的呢?我们来看下InterceptorServiceImpl的init方法:init()方法会在服务被创建后立即调用,如上所示就是遍历Warehouse.interceptorsIndex ,使用存储在其中的拦截器class对象 反射创建拦截器实例,然后存在存入 Warehouse.interceptors。 也即是说,ARouter初始化完成后就获取到了所有拦截器实例

那么Warehouse又是啥呢?interceptorsIndex是如何存储的所有拦截器的class的?

1.6路由原信息的仓库

Warehouse意为仓库,用于存放被 @Route、@Interceptor注释的 路由相关的信息,也就是我们关注的destination等信息。既然是仓库,那么就是有存有取

前面举的例子:moduleB发起路由跳转到moduleA的activity,moduleB没有依赖moduleA,只是在moduleA的activity上增加了@Route注解。 由于进行activity跳转需要目标Activity的class对象来构建intent,所以必须有一个中间人,把路径"/test/activity"翻译成Activity的class对象,然后moduleB才能实现跳转。(因此在ARouter的使用中 moduleA、moduleB 都是需要依赖 arouter-api的)

这个中间人那就是ARouter了,而这个翻译工所作用到的词典就是 Warehouse,它存着所有路由信息。

class Warehouse {
    //所有IRouteGroup实现类的class对象,是在ARouter初始化中赋值,key是path第一级
    //(IRouteGroup实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务)
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); 
    //所有路由元信息,是在completion中赋值,key是path
    //首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务
    static Map<String, RouteMeta> routes = new HashMap<>();
    
    //所有服务provider实例,在completion中赋值,key是IProvider实现类的class
    static Map<Class, IProvider> providers = new HashMap<>();
    //所有provider服务的元信息(实现类的class对象),是在ARouter初始化中赋值,key是IProvider实现类的全类名。
    //主要用于使用IProvider实现类的class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class)
    static Map<String, RouteMeta> providersIndex = new HashMap<>();
    
    //所有拦截器实现类的class对象,是在ARouter初始化时收集到,key是优先级
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("...");
    //所有拦截器实例,是在ARouter初始化完成后立即创建
    static List<IInterceptor> interceptors = new ArrayList<>();
...
}

Warehouse存了哪些信息呢?

  • groupsIndex所有路由组元信息。是所有IRouteGroup实现类的class对象,是在ARouter初始化中赋值,key是path第一级。IRouteGroup实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务)。
  • routes所有路由元信息。是在LogisticsCenter.completion中赋值,key是path。首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务
  • providers所有服务provider实例。在LogisticsCenter.completion中赋值,key是IProvider实现类的class
  • providersIndex所有provider服务元信息(实现类的class对象)。是在ARouter初始化中赋值,key是IProvider实现类的全类名。用于使用IProvider实现类class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class)
  • interceptorsIndex所有拦截器实现类class对象。是在ARouter初始化时收集到,key是优先级
  • interceptors所有拦截器实例。是在ARouter初始化完成后立即创建

其中groupsIndex、providersIndex、interceptorsIndex是ARouter初始化时就准备好的基础信息,为业务中随时发起路由操作(Activity跳转、服务获取、拦截器处理)做好准备。

那么Warehouse的信息是如何收集到的呢? 下面就先来看下ARouter初始化具体做了哪些事情:

final class _ARouter {
    ...
    protected static synchronized boolean init(Application application) {
        mContext = application;
        LogisticsCenter.init(mContext, executor);
        ...
        return true;
    }

_ARouter的init方法中 调用了LogisticsCenter的init方法。LogisticsCenter意为物流中心,上面提到的完善postcard的completion操作也是此类提供。

//LogisticsCenter.java
//LogisticsCenter初始化,加载所有的路由元信息
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {...
    try {
        long startInit = System.currentTimeMillis();
        //先尝试使用AGP transform 收集 根帮助类 后 写好的注入代码(要先引入插件才行 apply plugin: 'com.alibaba.arouter')
        loadRouterMap();
        if (registerByPlugin) {
            //registerByPlugin为true说明使用AGP加载ok了(通常都会用AGP,即registerByPlugin为true)
            logger.info(TAG, "Load router map by arouter-auto-register plugin.");
        } else {
            //若没有使用 AGP transform,就用ClassUtils.getFileNameByPackageName来搜集dex中ROUTE_ROOT_PAKCAGE包下的所有类,即编译时生成的所有帮助类
            //这样的话,就是运行时 遍历搜集 会比较耗时,也就是init会较为耗时;而AGP transform 是在编译时完成收集的。
            //当前app是新安装时才会走(收集到的帮助类会缓存到SP文件)
            Set<String> routerMap;
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                // 这写帮助类是在编译时由arouter-compiler生成
                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);
            } else {
                //不是新安装的版本,就从SP文件中读取
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }...
            //遍历帮助类,区分是哪种帮助类,然后反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map
            for (String className : routerMap) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Root
                    //填充Warehouse.groupsIndex,即所有IRouteGroup实现类的class对象
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Interceptors
                    //填充Warehouse.interceptorsIndex,即所有IInterceptor实现类的class对象
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    //类名开头:com.alibaba.android.arouter.routes.ARouter$$Providers
                    //填充Warehouse.providersIndex,即所有provider的RouteMeta
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }...
    }
}

LogisticsCenter初始化 就是加载所有的路由元信息的过程,有两种方式:

  1. 走loadRouterMap()方法:直接使用在编译时收集好的帮助类信息,然后反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。这需要开发者要先引入插件才行 apply plugin: 'com.alibaba.arouter'。如果加载成功,registerByPlugin这个值就为true,否则false。
  2. 若第一步没有加载成功,即registerByPlugin为false:就会使用ClassUtils.getFileNameByPackageName在运行时搜集dex中"com.alibaba.android.arouter.routes"包下的所有类(即帮助类),然后遍历,区分是哪种帮助类。接着反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。

两种方式对比:

  • 相同点:都是使用帮助类信息反射创建帮助类实例后,调用其loadInto方法来填充Warehouse相应的Map。
  • 不同点:在于帮助类信息的收集方式。前者是在编译时搜集就完毕了,后者是运行时。

一般都是使用第一种,因为运行时遍历dex搜集会比较耗时,而第一种在编译时已经收集好了。 第一种方式的loadRouterMap()方法的实现逻辑 我们稍后再看。

2.流程图

参考

“终于懂了” 系列:组件化框架 ARouter 完全解析(一) 原理详解 - 掘金

“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT技术 - 掘金

“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM—动态代码注入 - 掘金

阿里的ARouter,问几个你“不会”的问题!

ARouter.md

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

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

相关文章

武汉凯迪正大—全自动变比测试仪

产品介绍 武汉凯迪正大KDB-IV全自动变比组别测试仪可有效监督变压器产品出厂及使用过程中的质量&#xff0c;防止变压器匝间短路&#xff0c;开路&#xff0c;连接错误&#xff0c;调压开关内部故障或接触故障。 KDB-IV全自动变比组别测试仪在原基础上根据用户的现场使用要求…

【Android】adjustViewBounds 的理解和使用

理解 adjustViewBounds 是一个 ImageView 的属性&#xff0c;用于调整 ImageView 的边界以适应图像的尺寸。当设置为 true 时&#xff0c;ImageView 的边界将根据图像的宽高比例进行调整&#xff0c;以确保图像完全显示在 ImageView 内部。 理解和使用 adjustViewBounds 的步…

推荐一些网络安全的网站和论坛

今天小编给大家推荐一批网络安全的网站和论坛&#xff0c;想学习网络安全技术的朋友们可以去这些网站看看&#xff1a; 1、红黑联盟 红黑联盟论坛 网址&#xff1a;bbs.2cto.com 2、安全客 一个提供网络安全资讯的网站 网址&#xff1a;www.anquanke.com 3、T00ls T00ls是…

为什么说互联网是最坏的行业

犹如春心萌动&#xff0c;想来写点东西。最近突然想通了很多事&#xff0c;以前老逼着自己拼命的挣钱&#xff0c;放弃了一切私生活&#xff0c;没有朋友&#xff0c;没有聚会&#xff0c;连逛街都认为是浪费时间和生命。确实挣了一些钱&#xff0c;以为自己会开心会快乐&#…

易点易动固定资产管理系统:高效完成固定资产盘点任务

固定资产盘点是企业管理中不可或缺的重要环节。然而&#xff0c;传统的固定资产盘点方式往往效率低下、易出错&#xff0c;给企业带来不必要的麻烦和成本。为了解决这一问题&#xff0c;易点易动固定资产管理系统应运而生。本文将详细介绍易点易动固定资产管理系统在固定资产盘…

完美解决word转pdf(亲测有效)

word搞了一天,找了无数的资料,以下这种方法是最好的。。。什么只能转3页了。。。什么字体库了。。。。各种坑 就三步: 第一步:下载jar包,然后放在这里 第二步:引入jar包,放在pom.xml文件 <dependency><groupId>com.aspose</groupId><artifact…

向世界展示“中国品牌”实力,中海达参展INTERGEO

10月10日-12日&#xff0c;全球测绘地理信息行业最权威的盛会INTERGEO在德国柏林举行。本次展会以“Inspiration for a smarter world”&#xff08;灵感碰撞&#xff0c;为更智慧的世界&#xff09;为主题&#xff0c;这与参展的中海达企业愿景“让万物位置更精准&#xff0c;…

【Linux】线程安全问题①——如何实现资源访问互斥(附图解与代码实现)

线程安全主要分为两个方面&#xff0c;分别是资源访问互斥与线程同步&#xff08;线程协同配合&#xff09; 本篇博客&#xff0c;我们主要来讲解资源访问互斥这一方面 目录 为什么要实现资源访问互斥&#xff1f; 实现资源访问互斥&#xff08;原子访问&#xff09;的经典…

JMeter连接数据库

一. 下载数据库驱动jar包 https://jdbc.postgresql.org/download/ 二. 将数据库驱动放到jmeter的lib目录下 三. 在jmeter中引用这个jar包 四. 添加一个jdbc数据库连接配置 五. 添加一个jdbc request来查询sql并将查询结果赋值给一个变量 六. 将查询结果用于其他请求

fico入门基础

Fico模块 会计主体一般为公司法人 分公司不算一个会计主体 分公司上金融中心 子公司会算一个会计主体 子公司上公司代码 会计期间:就是会计会一个期间结算一次(一般为一个月结算一次)(不同国家的快递期间起点会有不同;如日本四月份为第一个快递期间,三月份为第十二个快递期…

OTN较WDM系统的 升级功能

文章目录 1、增加运维规则&#xff0c;具体措施是增加了帧结构&#xff0c;提升业务的监控管理运维能力。2、增加电交叉功能&#xff0c;使得OTN系统可以分别处理客户业务信号和波分信号。 OTN较WDM系统的 升级功能主要表现在下面两方面&#xff1a; 1、增加运维规则&#xff0…

驾校倒闭了这么多,凭什么我能活下来,因为有云表利剑

因为部门场地比较分散&#xff0c;学员名单都是通过wps传递&#xff0c;但是某个部门更新学员信息时&#xff0c;其他部门无法实时共享更新&#xff0c;导致其他部门无法掌握学员最新信息&#xff0c;为学员提供制定服务。用了云表后学员只需在一个部门办一次业务&#xff0c;其…

专题三:穷举、暴搜、深搜、回溯、剪枝【递归、搜索、回溯】

1、全排列 class Solution { public:vector<vector<int>> ret;vector<int> path;bool check[7];void dfs(vector<int>& nums){if(nums.size() path.size()) {ret.push_back(path);return;}for(int i 0;i < nums.size();i){if(check[i] fals…

2023 年值得关注的软件测试趋势

随着软件在商业和日常生活中继续发挥关键作用&#xff0c;测试将不断发展以满足现代数字环境的需求。因此&#xff0c;组织努力更快、更频繁地交付他们的软件&#xff0c;并且测试过程需要更多地集成到开发过程中。 到2023年&#xff0c;我们可以看到各种软件测试趋势的出现&am…

轻游戏风格虚拟资源付费下载模板Discuz论坛模板

轻游戏风格虚拟资源付费下载模板Discuz论坛模板&#xff0c;游戏资讯付费VIP源码模板。 模板说明&#xff1a; 1、模板名称&#xff1a;"qing游戏风格"&#xff0c;版本支持&#xff1a;discuzx3.0版本&#xff0c;discuzx3.1版本&#xff0c;discuzx3.2版本&#…

4.查询用户的累计消费金额及VIP等级

思路分析&#xff1a; &#xff08;1&#xff09;按照user_id及create_date 分组求消费金额total_amount &#xff08;2&#xff09;开窗计算同user_id下的累计销售金额sum(total_amount) over(partition by user_id order by create_date ROWS BETWEEN UNBOUNDED PRECEDING AN…

报错——warning: ignoring JAVA_HOME=/home/jdk/jdk1.8.0_281; using bundled JDK

我使用了es的8.3.0版本&#xff0c;但es从7.17版本以后不再支持jdk1.8了&#xff0c;需要进行JDK的版本升级&#xff0c;或者降低es的版本。 es和jdk对比版本

口袋参谋:如何写出高权重标题?用对这招很重要!

​如何写出高权重标题&#xff1f;这是99.99%的卖家都存在的疑虑&#xff01; 以前写高权重标题&#xff0c;很多卖家往往会复制同行竞品爆款标题到淘宝首页搜索框&#xff0c;然后在全标题后面加上几个字母&#xff0c;就可以拆分爆款标题。 这个问题我之前也说过&#xff0…

【Java基础面试三】、说一说你对Java访问权限的了解

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;说一说你对Java访问权限…

css3自动吸附scroll-snap

我们希望可以一块一块的滚动&#xff0c;比如当前一个块滚出去了一部分并且后一个块滚进来一部分的时候&#xff0c;实现后一个块自动滚入或者前一个块回弹到初始位置这种效果&#xff0c;以前的时候用js需要写比较复杂的判断逻辑&#xff0c;后来有了一个css scroll snap这个方…