init与zygote的启动流程

news2024/11/18 3:35:23

文章目录

  • 1.init进程启动过程
    • 1.1init进程的入口函数
      • 1.1.1创建和启动所需要的文件目录
      • 1.1.2对属性进行初始化与启动属性服务
      • 1.1.3子进程信号处理函数
      • 1.1.4.重启死去的进程
      • 1.1.5.解析init.rc配置文件
    • 1.2解析init.rc配置文件
    • 1.3解析Service语句
    • 1.4init启动Zygote
      • 1.4.1start方法
    • 1.5属性服务
      • 1.5.1start_property_service
      • 1.5.2服务传理客户端请求
    • 1.6总结init进程
  • 2.Zygote启动流程
    • 2.1启动Zygote进程
      • 2.1.1runtime.start()的前戏
      • 2.1.2runtime.start()
        • 2.1.2.1JNI
      • 2.1.3ZygoteInit的main()方法的实现
        • 2.1.3.1启动SystemServer
          • 2.1.3.1.2创建子进程的作用
        • 2.1.3.2runSelectLoop
    • 2.2总结

1.init进程启动过程

init进程是Android系统中用户空间的第一个进程,进程号就是1,手机开机后,并不会直接进入init进程,在它的前面还有几步流程

  1. 启动电源,引导芯片代码从预定的地方(固化在ROM)开始,加载引导程序BootLoaderRAM,然后执行
  2. 引导程序BootLoader在Android操作系统程序开始之前的一个小程序,把系统OS拉起来并运行
  3. 启动Linux内核,寻找init.rc文件,启动init进程

上面的话有点多,而且有点难以理解,我对它的看法就是

1.启动电源

2.加载引导程序

3.启动Linux内核

4.启动init进程

1.1init进程的入口函数

刚才我们说过,在启动完linux内核之后就会寻找init.rc,然后启动init进程

我们要是找到Android的系统源码的话,我们就能发现,init目录下的所有文件就组成了init进程的整体,它的main.cpp文件就是它的入口函数

代码就不列了,我们主要知道init的这个入口函数里面到底做了哪些事情?

大致可以分为5部

1.1.1创建和启动所需要的文件目录

init的main()函数里面主要挂载了tmpfs,devpts,proc,sysfs和selinuxfs5种文件系统,这五种文件系统只有在系统运行的时候才会存在

1.1.2对属性进行初始化与启动属性服务

在main()中

property_init();

用这段代码进行初始化

后面

start_property_service();

这段进行启动属性服务

1.1.3子进程信号处理函数

main函数有一段这个代码

signal_handler_init();

不看解释我以为又初始化某个属性,结果它的作用是如果init的子进程(zygote进程)异常退出的话,init进程可能会不知道zygote进程退出了,在系统表里面还为这个进程保留了一定的信息(进程号,退出状态,运行时间等等),导致进程表的资源被浪费。为了防止zygote进程成为这样的僵尸进程,为此用这段代码,用来接收zygote进程终止的SIGCHLD信号

1.1.4.重启死去的进程

restart_process();

1.1.5.解析init.rc配置文件

parser.ParseConfig("/init.rc");

我们重点看一下如何解析init.rc配置文件的

1.2解析init.rc配置文件

init.rc这个配置文件极为重要,我们在上面不也说过了嘛,它由5种类型的语句构成的分别是

Action,Command,Service,Option,Import

其中我们上面说的zygote进程,主要是由Service类型语句构成的,Service语句用来通知init进程创建一个名为zygote的进程

1.3解析Service语句

一般情况下我们用ServiceParser来解析Service语句,

这个挺好记的,Service是服务,Parser为解析,服务+解析,所以它就是用来解析Service语句的

ServiceParser解析Service语句通常会用到两个函数

  1. ParseSection
  2. ParseLineSection

看这两个函数长得其实也差不多,但是前者是用来解析Service中的rc文件,比如init.zygote64.rc,然后就可以定义Zygote进程的属性、启动服务、设置文件权限、挂载文件系统等

但是这个并不等于就可以直接启动zygote进程,ParseSection这个函数只是Zygote进程启动的前置工作之一,它确保了Zygote进程在启动后能够正确地读取配置文件中定义的属性和服务,并执行相应的操作。但解析init.zygote64.rc文件并不代表Zygote进程已经完全启动。

后者,ParseLineSection这个函数用来解析子项,这句话的意思是,ParseLineSection是用来解析init.zygote64.rc文件中的子项的

这句话怎么理解,它和ParseSection解析的init_zygote64.rc解析的范围有什么不一样的

ParseSection函数用来解析init.zygote64.rc文件中的段落,而ParseLineSection函数用来解析init.zygote64.rc文件中的子项。在解析init.zygote64.rc文件时,Zygote进程会先使用ParseSection函数解析整个文件,然后使用ParseLineSection函数解析每个子项

在ServiceParser中我们会通过

service = std::make_unique<Service>(name,str_args);

来创建Service对象,这个Service对象就是Android中四大组件的那个Service

ServiceParser中调用完ParseSectionParseLineSection后才会调用EndSection

EndSection中会调用ServiceManagerAddService函数,这个AddService会调用

service_.emplace_back(std::move(service));

这个就是把内容填充到service后将service对象加入vector类型的service链表中

1.4init启动Zygote

在刚才我们已经将Service放入到Vector类型service链表中

既然放入到了链表中,那么就说明service对象很多,所以我们现在的任务就是从service链表中选出那个zygote,因为zygoteclassNamemain,所以我们从链表中找到classNamemain的那个就可以了

因为zygote对应的init.zygote64.rc并没有设置disabled选项,所以我们得通过调用start方法,才能启动zygote

1.4.1start方法

我们刚才说了因为没有设置disabled选项所以我们必须得调用start方法才能启动zygote,所以我们来看来看看start的方法是怎么实现的

1.首先它会判断servicce是否是否启动,如果启动了的话,那就不管了直接返回一个false,如果没有启动,那么就进行下面的操作

2.它判断子进程有没有启动,如果没有启动那么就通过fork()来创建一个子进程(这个在fork()在操作系统里面经常遇见),并返回pid的值,如果pid的值为0,那么就通过execve函数的调用,启动service的子进程

3.在启动子进程的同时会进入该Service的main()函数,如果该servicezygote则会调用runtimestart方法,这样就成功启动了zygote

当时我有两个地方不太懂,

第一个是为什么要创建一个子进程,第二个是pid==0这意味着什么?

结合网上查到的资料与我自己的理解

对于为什么要创建一个子进程,理解是当应用程序进程启动时,Zygote 进程会派生出一个新的应用程序进程,并将应用程序的类和资源加载到该进程的 Dalvik 虚拟机中。起到提高启动速度和效率与降低资源浪费的作用

而pid0则是因为在Android系统中,进程ID0表示虚拟进程,它不占用系统资源,而是用来管理系统的一些特殊任务。这里的Zygote进程与后面的Binder驱动程序就是和这个有关

对了在嵌入式里面也有pid,但那是一个算法和这个没有关系

1.5属性服务

这里的属性服务指的是即使系统或软件重启,还是可以根据之前注册表中的记录,进行相应的初始化工作

这个和之前讲的Activity中的一个特别相似

在讲Activity中,如果我们不使用ViewModel的话,在把手机从竖屏到横屏的时候,它会执行onDestroy(),但是假如我们那个Activity中有EditText,EditText里面有TextView,为什么TextView不会丢失

我们把它的生命周期打印下来

 D/TAG: onCreate
 D/TAG: onStart
 D/TAG: onResume
 D/TAG: onPause
 D/TAG: onStop
 D/TAG: onSaveInstanceState
 D/TAG: onDestroy
 D/TAG: onCreate
 D/TAG: onStart
 D/TAG: onRestoreInstanceState
 D/TAG: onResume

我们注意一下,这里有一个onSaveInstanceStateonRestoreInstanceState,就是因为有这个,在旋转的时候通过onSaveInstanceState把我们的EditText中的数据保存下来又通过onRestoreInstanceState将保存的数据弄到EditText中

这个的作用和属性服务很像

我们在init进程的入口函数的代码中也讲过,init的其中一个功能就是对属性进行初始化与启动属性服务

property_init();

start_property_service();

其中property_init()在《进阶解密》中并没有讲多少只是提了一嘴可以初始化属性内存区域

主要讲的是start_property_service

我们就主要看看start_property_service主要干了些什么

1.5.1start_property_service

简单说一下就是4个点

  1. 创建一个非阻塞的socket
  2. 调用listen方法对property_set_fd进行监听,使socket成为一个属性服务
  3. property_set_fd放入epoll进行监听
  4. property_set_fd有数据到来的时候,init进程会调用handler_property_set_fd进行处理

首先socket我们都很清楚,它是IPC的一个方式,我用Java写聊天室就是通过socket写的

主要是第二点和第三点到底是什么意思,listen和epoll感觉都是对property_set_fd进行监听,那么这两个监听又有什么不同呢?

我搜了一下发现,listen主要监听的是property_set_fd的属性

而epoll监听的是多个套接字,比如监听IPC

chatgpt给出的解释是

listen_socket() 函数用于创建 Unix 套接字并监听属性变化的通知,而 epoll_wait() 函数用于监听套接字状态的变化,并作出相应的响应。

那么就把它理解成listen比epoll多一个创建套接字的过程,epoll又比listen多一个响应的过程

在handler的那块我写过,当MessageQueue没有Message传递给Looper的时候,在epoll会调用epoll.await来阻塞主线程,这一块联系到一起了

1.5.2服务传理客户端请求

我们上面说了当property_set_fd有数据到来的时候,init进程会调用handler_property_fd进行处理

系统属性分为2种类型,一种是普通属性,一种是控制属性。控制属性的一个例子就是开机动画

既然属性都分为这两种了,那么处理属性的也分为两种

一种是处理普通属性,一种是处理控制属性一般以 ctl.开头的就是控制属性

因为控制的英文单词是control

如果是普通属性则会对普通属性进行修改,首先判断属性合不合法,然后判断属性存不存在,不存在就添加,存在就更新

1.6总结init进程

init进程主要就是干了以下三点

  1. 创建和挂载启动所需要的文件目录
  2. 初始化和启动属性
  3. 解析init.rc配置文件并启动zygote进程

2.Zygote启动流程

2.1启动Zygote进程

2.1.1runtime.start()的前戏

在刚才init的启动流程中我们还记得,Zygote的启动是在init解析init.rc文件后,解析service,然后在service链表中找到那个className为main的service,调用它的start方法,start方法在内部会先判断它是否启动这个service,没有启动的话判断是否有service子进程在没有的情况下先通过fork创建子进程然后找到pid==0的那个进程启动service的子进程然后进入service的main函数,调用runtime.start()来启动Zygote进程

但是runtime是怎么调用的我们并不是多熟悉,所以了解Zygote的第一步,就是先了解一下runtime在调用之前它的那个函数都经历过什么?

我们先找到frameworks/base/cmds.app_process/app_main.cpp中找到代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oRtqeBm-1686922381178)(../../assets/QQ图片20230616192925.jpg)]

这段代码可以理解做了3件事。

第一件事是判断main函数在不在zygote进程中,如果在的话把标志位设置为true

第二件事是如果不在zygote进程中,那么就再判断是否运行在SystemServer进程中,是的话把它的相关标志位标志为true

第三件事是如果zygote进程中的标志为true,那么就调用runtimestart

然后我们看看runtime.start()代码里面的东西

2.1.2runtime.start()

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
	.....
//const char* kernelHack = getenv("LD_ASSUME_KERNEL");
//ALOGD("Found LD_ASSUME_KERNEL='%s'\n", kernelHack);

/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { //1 启动虚拟机
    return;
}
onVmCreated(env); //2 创建虚拟机

/*
 * Register android functions.
 */
if (startReg(env) < 0) { //3 注册Java方法
    ALOGE("Unable to register all android natives\n");
    return;
}

/*
 * We want to call main() with a String array with arguments in it.
 * At present we have two arguments, the class name and an option string.
 * Create an array to hold them.
 */
jclass stringClass;
jobjectArray strArray;
jstring classNameStr;

stringClass = env->FindClass("java/lang/String");
assert(stringClass != NULL);
strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
assert(strArray != NULL);
classNameStr = env->NewStringUTF(className);//4 获得类名
assert(classNameStr != NULL);
env->SetObjectArrayElement(strArray, 0, classNameStr);

for (size_t i = 0; i < options.size(); ++i) {
    jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
    assert(optionsStr != NULL);
    env->SetObjectArrayElement(strArray, i + 1, optionsStr);
}

/*
 * Start VM.  This thread becomes the main thread of the VM, and will
 * not return until the VM exits.
 */
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName); //5 找到类名对应的类--ZygoteInit
if (startClass == NULL) {
    ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
    /* keep going */
} else {
    jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
        "([Ljava/lang/String;)V");//6 找到对应类的main方法
    if (startMeth == NULL) {
        ALOGE("JavaVM unable to find main() in '%s'\n", className);
        /* keep going */
    } else {
        env->CallStaticVoidMethod(startClass, startMeth, strArray);//7 调用找到的main方法,从这里开始也进入了Java框架层
        #if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
	......
}

我们是不是都会有点疑虑,我们的Android代码要么用java要么用kotlin,在native层才用c++语言,那什么时候native层才会结束

答案等会儿就可以揭晓了

我们可以看到runtime.start()代码的内部它先创建了一个Java虚拟机,然后在java虚拟机中注册了JNI方法

通过JNI方法,我们调用ZygoteInit的main方法,成功让Zygote由Native层转到Java框架层

这后面我们看到的代码就是java的了

我们本来应该乘胜追击看看ZygoteInit里面的main()方法是怎么实现的,但是在此之前,我们先来看看什么叫JNI?

2.1.2.1JNI

JNI(Java Native Interface)方法是一种 Java 编程语言本地(即非 Java)编程语言交互的机制。它允许 Java 应用程序调用本地代码(通常是 C 或 C++ 代码),从而实现对底层系统或硬件的直接访问

2.1.3ZygoteInit的main()方法的实现

代码有点长,所以我还是用我自己的表达来叙述,

一般代码太长的,我也不会去看它,而是去看相关的注释

它的第一步是创建一个Server端Socket

我们是通过

zygoteServer.registerServerSocket(socketName);

来完成,我们可以看见它是通过这个ZygoteServer来调用registerServerSocket

然后第二步是

preload(bootTimingsTraceLog);

这步是用来预加载类和资源

第三是调用

startSystemServer(abiList,socketName,zygoteServer);

我们把上面注册Server端的Socket还有socketName还有abiList这3个参数传入来启动SystemServer

第四步通过

zygoteServer.runSelectLoop(abiList);

用这个方法来等待AMS请求

第一步创建Server端的Socket和第二步预加载类和资源这个没什么说的,我们重点来看3,4这2个步骤

2.1.3.1启动SystemServer

我们通过创建一个args[]数组,因为我们是要启动SystemServer,所以很明显这个数组是用来存储启动SystemServer的参数的

然后把这个数组封装成forkSystemServer函数所需要的参数,通过这些参数,我们可以创建子进程,如果创建的这个子进程的pid==0就代表当前代码运行在新创建的子进程中,在这个基础上,我们调用handlerSystemServerProcess()来处理SystemServer进程

这样启动就算结束了

但是我有一点不明白的就是当时候我们init进程启动的时候创建了一个子进程,然后当它的pid==0的时候才能执行start方法

然后在启动SystemServer的过程中我们也要创建一个子进程,当它的子进程的pid==0的时候才能继续进行**handlerSystemServerProcess()**来进行下面的处理

那么这个创建子进程到底是在干什么

2.1.3.1.2创建子进程的作用

Zygote 进程是一个特殊的进程,它主要负责启动其他的 Java 进程和应用程序。在启动一个新的进程时,如果每次都需要重新启动一个 Zygote 进程,会导致系统资源的浪费和进程启动速度的下降。因此,在 Android 系统中,通常会使用 fork() 函数来创建一个新的进程,从而提高进程启动速度和系统资源利用率。

forkSystemServer() 函数的作用就是使用 fork() 函数来创建一个新的进程,并在新进程中执行 handlerSystemServerProcess() 函数。这个新进程就是 SystemServer 进程,它是 Android 系统中非常重要的进程之一,负责管理系统服务、应用程序和其他系统组件。使用 forkSystemServer() 函数可以快速启动 SystemServer 进程,从而提高 Android 系统的启动速度和稳定性。

另外,forkSystemServer() 函数还实现了子进程和父进程之间的通信,通过 socket 描述符来共享信息。这种方式可以实现进程间的同步和数据共享,从而提高系统的协作能力和效率。

2.1.3.2runSelectLoop

我们看到Loop我们就可以猜到它肯定又是一个进行无限循环然后把相关消息交给其它进行处理

果不其然

我们在它的源码看到的最显眼的就是那个

while(true)

runSelectLoop的主要作用就是监听刚才创建Server端的Socket,通过这个runSelectLoop无限循环来等待AMS的请求并创建新的应用程序的进程

2.2总结

我们以它处于Native还是Java框架层为分界点,分为两个时期

第一段是仍然处于Native层,我们先判断service的main函数在不在Zygote进程,如果在的话,给它相关的标志设置为true。如果不在,但是它在SystemServer这个进程中,那么我们把这个的相关标志设置为true。在Zygote的相关标志为true的情况下,我们启动Zygote进程

start的内部我们会创建Java虚拟机,然后注册JNI方法,通过JNI方法 我们调用ZygoteInit的main方法,成功让Zygote由Native层转到Java框架层

从此第一阶段结束

第二阶段就是我们通过JNI方法创建ZygoteInitmain方法之后进入java框架层

在这里面我们主要进行了4个操作

第一个操作是创建了Server端的Socket,以供后面第四步的runSelectLoop使用

第二个操作就是预加载类和资源

第一个和第二个操作没有怎么讲,因为感觉确实没啥讲的

第三个操作应该是监听,但是它的方法竟然在启动SystemServer后面就很离谱,监听调用的就是runSelectedLoop()方法,监听第一步获得的Server端的Socket然后等待AMS请求并对相关的套接字做出回应

第四个操作是启动SystemServer,我们通过创建子进程,然后判断子进程的pid是否为0,如果为0的话,那么我们就**handlerSystemServerProcess()**来处理SystemServer。(这个步骤和init启动Zygote的步骤极为相似)

至此Zygote进程的启动流程结束

我们可以看出它的主要的作用

1.从Native层跳到Java的虚拟机层

2.无限循环监听AMS的请求,并作出回应

3.启动SystemServer
main方法之后进入java框架层

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

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

相关文章

Android kotlin 实现仿淘宝RecyclerView和对应下的指示器功能

文章目录 一、实现效果二、引入依赖三、源码实现1、指示器样式(自定义类)2、RecyclerView适配器3、主视图实现一、实现效果 指示器样式,第二个gif是用模拟器的,gif有小问题,第三个截图没问题 二、引入依赖 在app的build.gradle在添加以下代码 1、implementation com.githu…

数字电路基础---组合逻辑

目录 数字电路基础---组合逻辑 1、简介 2、实验任务 3、程序设计 4、仿真验证 数字电路基础---组合逻辑 FPGA 或者 IC 内部的逻辑一般包括组合逻辑和时序逻辑&#xff0c;组合逻辑一般指的是一些门电路或者选择器、比较器一起组成的逻辑。 1、简介 数字电路根据逻辑功能的…

程序员必备超好用下载器IDM(Windows下载)

程序员必备现役最强下载器IDM&#xff08;Windows下载&#xff09; 1 介绍 IDM&#xff0c;全称Internet Download Maneger&#xff0c;在下载界可谓是大名鼎鼎&#xff0c;不仅仅是其强大的32线程下载&#xff0c;还因为其无与伦比的资源嗅探功能&#xff0c;Internet Downlo…

监听关闭浏览器触发事件

关闭和刷新页面都会触发&#xff0c;一般都不用来做弹窗提示&#xff0c;一般用来做数据操作 // 监听页面关闭 清除本地缓存 window.onbeforeunload function (e) { localStorage.removeItem("statement"); }; // 监听页面关闭 提醒是否关闭 现在不允许自定义内容了…

生命周期监听的使用和源码解析

定义SpringApplicationRunListener来监听springApplication的启动 1.通过实现springApplicationRunListener来实现监听。 2.在 META-INF/spring.factories 中配置 org.springframework.boot.SpringApplicationRunListener自己的Listener。 在默认的springboot配置中就有给我…

主数据概念过时了吗,在这些大数据技术背景下,数据平台等新技术的出现,我们还需要主数据吗?

当企业信息化发展到一定程度时, 数据管理必然会被提升为企业的一个重要管理领域。数据管理的好坏程度, 很大程度上影响着企业信息化进程, 决定着企业信息化最终的成效。 企业信息化建设基本上都是从部门级开始的, 从部门的实际业务需要出发独立建设信息系统, 随着这些信息系统…

2023年地图产业研究报告

第一章 行业概况 地图行业是一个快速发展且关键的信息技术领域&#xff0c;通过收集、处理和可视化地理信息&#xff0c;为用户提供导航、位置服务、地理信息系统和地图数据分析等应用。地图行业的发展受益于全球定位系统&#xff08;GPS&#xff09;技术的进步和移动设备的普…

K-verse 合作伙伴访谈|Dalgomz 将韩国旅游体验带入元宇宙

■ 请向我们介绍 Dalgomz 。 你好&#xff01;我是 Yong-Min Park&#xff0c;Dalgomz 项目的首席执行官。我们的项目专注于通过使用区块链技术创建旅游福利会员计划来促进韩国江原道雪岳地区的旅游业。从雪岳地区开始&#xff0c;Dalgomz NFT 持有者在韩国各地旅行时可以获得现…

基于局部自适应滤波器的双边滤波器matlab完整代码分享

双边滤波(Bilateral filter)是一种可以保边去噪的滤波器。其输出像素的值依赖于邻域像素的值的加权组合。从效果来说,双边滤波可产生类似美肤的效果。皮肤上的皱纹和斑,与正常皮肤的差异,远小于黑白眼珠之间的差异,因此前者被平滑,而后者被保留。如图所示(右图为经过处…

Oracle 查询优化改写(第五章)

第五章 使用字符串 1.遍历字符串 SELECT 天天向上 内容&#xff0c;level&#xff0c;substr(天天向上, LEVEL, 1) 汉字拆分FROM Dual CONNECT BY LEVEL < Length(天天向上);2.计算字符在字符串中出现的次数 3.从字符中删除不需要的字符 若员工姓名有元音字母AEIOU&#x…

pikachu靶场-敏感信息泄露

敏感信息泄漏简述 攻击方式 常见的攻击方式主要是扫描应用程序获取到敏感数据 漏洞原因 应用维护或者开发人员无意间上传敏感数据&#xff0c;如 github 文件泄露 敏感数据文件的权限设置错误&#xff0c;如网站目录下的数据库备份文件泄露 网络协议、算法本身的弱点&#xff…

阿里云ACK worker节点硬盘扩盘

1、在控制台选择要扩盘的服务器&#xff0c;进入硬盘并选择扩盘 2、进入盘后点击已备份扩盘 3、选择扩盘方式以及要扩盘的最后大小 4、确认是否按我们的要求扩盘的 5、扩盘分区和文件系统 6、手动扩盘操作 ACK扩盘文档 查看分区情况 [rootiZj6caed975ouwzuyu564vZ ~]# ls…

C++技能 ( 1 ) - 使用Lambda表达式【详解】

系列文章目录 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 C技能系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream w…

SpringBoot项目启动成功但接口访问404

问题描述&#xff1a;SpringBoot项目启动成功&#xff0c;但使用postman调接口时报404 分析&#xff1a;找不到路径&#xff0c;controller未生效。 解决&#xff1a;将com.local.coupon.template.templateimpl改为com.local.coupon.template。SpringBoot会扫描到com.local.co…

2023-6-16-第七式桥接模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

外包能转正吗?外包员工能变正式员工吗?

外包员工能变正式员工吗&#xff1f;这里辟谣一波&#xff0c;许多外包都说有转正机会。实际情况是几乎等于零。其中&#xff0c;三方外包更是可以直接和零划等号。三方外包的转正&#xff0c;往往就是给个内推机会&#xff0c;然后和面试官会熟悉一些。 然而这些都没什么价值…

【SpringCloud入门】-- 初识Eureka注册中心

目录 1.SpringCloudEureka简介 2.什么是CAP原则&#xff1f; 3.注册中心的概念&#xff1f; 4.SpringCloud其他注册中心 5.搭建Eureka注册中心 6.eureka的配置(主要是server&#xff0c;instance&#xff0c;client) 7.eureka集群概念 8.eureka集群搭建 1.SpringCloudE…

Unity 之 使用后处理的方式实现暗角效果

Unity 之 后处理URP工程实现边角压暗效果 一&#xff0c;URP工程配置二&#xff0c;代码调用三&#xff0c;实现原理 一&#xff0c;URP工程配置 在Hierarchy界面&#xff0c;创建空物体 GameObject&#xff0c;右键选择Volume菜单下的Global Volume。 创建后的结果&#xff1…

ahut 月赛1

心得: 一点一点理解&#xff0c;对于一段要学习的代码&#xff0c;跟着写下来&#xff0c;理解一点写一点 对于一道题目&#xff0c;用记事本&#xff0c;看题目&#xff0c;看一句题目&#xff0c;用自己的话概括一句&#xff0c;写在记事本上&#xff0c;并将自己的 想法一…

在JavaScript中的数据结构(链表)

文章目录 链表是什么&#xff1f;链表的好处详细的看一下列表单向链表实操链表向链表尾部追加元素从链表中移除元素根据元素的值移除元素在任意位置插入元素查找链表是否有改元素检查链表是否为空检查链表的长度查看链表头元素把LinkedList对象转换成一个字符串打印链表元素 双…