openjdk使用_深入地理解

openjdk使用_深入地理解目录前言一、从JDK源码看双亲委派二、使用步骤1.引入库2.读入数据总结前言关于JVM类加载的基础理论知识,请参照《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(上)和《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(下)。一、从JDK源码看双亲委派注:博主是使用的是openjdk8,换了新电脑还没有去编译源码,所以看的是静态代码,关于如何编译和调试源码,网上不少文章都有介绍,这里就不赘述了我们都知道在Java类加载中,除了BootStrap加载器,App和Ext加载

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺


前言

  关于JVM类加载的基础理论知识,请参照《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(上)《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(下)

一、从JVM源码看类加载器

  注:使用的是openjdk8

1.1 Java层面的类加载器

  我们都知道在Java类加载中,除了BootStrap加载器,App和Ext加载器都是Java实现的,具体实现在sun.misc.Launcher中:

public class Launcher{ 
   
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    ......
    public Launcher() { 
   
        Launcher.ExtClassLoader var1;
        try { 
   
            //没有显示设置父类加载器,为BootStrapClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) { 
   
            throw new InternalError("Could not create extension class loader", var10);
        }

        try { 
   
            //设置ExtClassLoader为父类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) { 
   
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置TCCL
        Thread.currentThread().setContextClassLoader(this.loader);
        ......
    }
    ......
}

  在Launcher类中,有一个静态私有成员变量launcher的赋值,调用本例的构造方法生成一个Launcher实例。因为launcher是一个类级别属性,所以这个操作会被收敛到类构造器<clinit>()方法,在该类被加载的初始化阶段被执行。

  在Launcher的构造方法中,分别初始化了Launcher.ExtClassLoader和Launcher.AppClassLoader加载器,将Launcher.loader属性(ClassLoader.getSystemClassLoader方法返回的就是这个属性)设置为了AppClassLoader,并且将TCCL设置为了AppClassLoader。ExtClassLoader和AppClassLoader初始化如下:

		//ExtClassLoader
		static class ExtClassLoader extends URLClassLoader { 
   
        private static volatile Launcher.ExtClassLoader instance;

        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { 
   
            if (instance == null) { 
   
                Class var0 = Launcher.ExtClassLoader.class;
                synchronized(Launcher.ExtClassLoader.class) { 
   
                    if (instance == null) { 
   
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }
}

		//AppClassLoader
		static class AppClassLoader extends URLClassLoader { 
   
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { 
   
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { 
   
                public Launcher.AppClassLoader run() { 
   
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
}

  从初始化方法就可以看出,ExtClassLoader没有显示设置父加载器,所以其父类加载器是BootStrap,AppClassLoader设置ExtClassLoader为自己的父加载器。

注:加载器的父子关系不是继承上的父子关系,而是通过成员变量引用,以组合的方式实现的父子关系

1.2 JVM是如何启动的

  程序的主要入口点在main.c,代码中有大量的条件编译,我们直接看JLI_Launch函数:

return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), const_jargs,
                   sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
                   FULL_VERSION,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
                   const_cpwildcard, const_javaw, const_ergo_class);

  JLI_Launch函数定义在java.h中,java.c中有该函数的实现,其中会调用LoadJavaVM函数,LoadJavaVM函数对于不同的平台(win、mac、solaris等)有不同的实现,我们这里看看windows的版本,实现在java_md.c中,主要是从jvmpath加载dll,并且初始化调用函数:

jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{ 
   
	//加载Microsoft环境c运行时库,提供必要的函数库调用和启动函数
	//后面创建线程启动JVM就使用的c运行时库函数_beginthreadex
    LoadMSVCRT();
    /* 根据jvmpath加载dll文件 */
    if ((handle = LoadLibrary(jvmpath)) == 0) { 
   
        JLI_ReportErrorMessage(DLL_ERROR4, (char *)jvmpath);
        return JNI_FALSE;
    }
    /* Now get the function addresses */
    ifn->CreateJavaVM =
        (void *)GetProcAddress(handle, "JNI_CreateJavaVM");
    ifn->GetDefaultJavaVMInitArgs =
        (void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");
    if (ifn->CreateJavaVM == 0 || ifn->GetDefaultJavaVMInitArgs == 0) { 
   
        JLI_ReportErrorMessage(JNI_ERROR1, (char *)jvmpath);
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

  加载Microsoft环境c运行时库后,会根据jvmpath加载jvm的dll文件(在jre目录存有动态链接文件,若将jre\bin\server下的jvm.dll移除,也启动不了JVM)。InvocationFunctions定义在java.h中,有三个JNI函数:

typedef struct { 
   
    CreateJavaVM_t CreateJavaVM;
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
    GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;

  关于jvmpath,需要再回到JLI_Launch函数中,在该函数中会调用CreateExecutionEnvironment函数创建执行上下文,在其中会初始化jvmpath:

CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg,  sizeof(jvmcfg));

  同样的在java_md.c中找到该函数win平台的实现:

void CreateExecutionEnvironment(int *pargc, char ***pargv,
                           char *jrepath, jint so_jrepath,
                           char *jvmpath, jint so_jvmpath,
                           char *jvmcfg,  jint so_jvmcfg) { 
   
	......
	/* 寻找要使用的JRE路径*/
    if (!GetJREPath(jrepath, so_jrepath)) { 
   
        JLI_ReportErrorMessage(JRE_ERROR1);
        exit(2);
    }
    /*获取jvm类型*/
    jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE);
	jvmpath[0] = '\0';
	/*根据JRE路径和jvm类型获取JVMPath*/
	if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath)) { 
   
        JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);
        exit(4);
    }
	......
}

  我们可以使用XXaltjvm参数传递JVM类型,在CheckJvmType函数中会完成检查工作。回到LoadJavaVM函数中,会根据jvmpath加载dll文件,然后初始化JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs函数地址。
  回到JLI_Launch函数中,除了LoadJavaVM之外,还会选择JRE版本、解析参数、设置classpath等等。准备工作做完之后,进入JVMInit函数执行JVM初始化流程,同样进入win的实现,在java_md.c中:

int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
        int argc, char **argv,
        int mode, char *what, int ret)
{ 
   
    ShowSplashScreen();
    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

  然后进入ContinueInNewThread函数,实现在java.c:

int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                    int argc, char **argv,
                    int mode, char *what, int ret)
{ 
   
	......
    { 
    /* 创建一个新的线程去创建JVM并且调用main方法*/
      JavaMainArgs args;
      int rslt;

      args.argc = argc;
      args.argv = argv;
      args.mode = mode;
      args.what = what;
      args.ifn = *ifn;
      rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
  	  ......
    }
}

  记住这里ContinueInNewThread0函数传的第一个参数是JavaMain。然后我们进入win下的实现,在java_md.c中看看ContinueInNewThread0函数的实现:

/* * Block current thread and continue execution in a new thread */
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { 
   
    int rslt = 0;
    unsigned thread_id;

#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
#define STACK_SIZE_PARAM_IS_A_RESERVATION (0x10000)
#endif

    /* * STACK_SIZE_PARAM_IS_A_RESERVATION is what we want, but it's not * supported on older version of Windows. Try first with the flag; and * if that fails try again without the flag. See MSDN document or HotSpot * source (os_win32.cpp) for details. */
    HANDLE thread_handle =
      (HANDLE)_beginthreadex(NULL,
                             (unsigned)stack_size,
                             continuation,
                             args,
                             STACK_SIZE_PARAM_IS_A_RESERVATION,
                             &thread_id);
    if (thread_handle == NULL) { 
   
      thread_handle =
      (HANDLE)_beginthreadex(NULL,
                             (unsigned)stack_size,
                             continuation,
                             args,
                             0,
                             &thread_id);
     ......

  _beginthreadex是一个c运行时库函数,其中:

  • arg1:安全属性,NULL为默认安全属性
  • arg2:线程堆栈大小,如果为0,则线程堆栈大小和创建它的线程的相同,在JVM参数- 中可以使用-Xss参数影响,该参数的解析在java.c的AddOption函数中,还包括-Xmx、-Xms的解析,这里就不贴代码了
  • arg3:线程调用的函数地址(函数名称),在ContinueInNewThread函数中传入的是JavaMain,所以最终执行的是JavaMain函数
  • arg4:传递给线程的参数指针
  • arg5:线程初始状态
  • arg6:记录threadId地址

  关于STACK_SIZE_PARAM_IS_A_RESERVATION,在os_windows.cpp中能找到说明:

Windows XP added a new flag ‘STACK_SIZE_PARAM_IS_A_RESERVATION’ for CreateThread() that can treat ‘stack_size’ as stack size. However we are not supposed to call CreateThread() directly according to MSDN document because JVM uses C runtime library. The good news is that the flag appears to work with _beginthredex() as well.

  我们只需要关心线程调用的函数:JavaMain。该函数实现在java.c中,该函数内容不少,主要是初始化JVM,加载MainClass,调用函数入口方法(main方法)等,下面是部分代码:

int JNICALL JavaMain(void * _args)
{ 
   
	......
    RegisterThread();

    /* 初始化虚拟机*/
    start = CounterGet();
    if (!InitializeJVM(&vm, &env, &ifn)) { 
   
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    //加载主类
    mainClass = LoadMainClass(env, mode, what);
    //获取main方法
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");
    CHECK_EXCEPTION_NULL_LEAVE(mainID);
    /* 调用main方法 */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    ......
}

  我们进入InitializeJVM函数简单看看初始化虚拟机的逻辑。InitializeJVM方法同样实现在java.c文件中:

static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{ 
   
    JavaVMInitArgs args;
  	......
    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}

  其中调用的CreateJavaVM函数,在前面已经初始化,使用JNI来调用JNI_CreateJavaVM(jvm.dll)。

    [DllImport("jvm.dll")]      public unsafe static extern int  JNI_CreateJavaVM(void** ppVm, void** ppEnv, void* pArgs);

1.3 C++层面的类加载器

  前面我们简单介绍了jvm的启动过程,进入jni.cpp中,调用Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);函数创建jvm,对于can_try_again参数,代码中给出了注释说明:

Certain errors during initialization are recoverable and do not prevent this method from being called again at a later time (perhaps with different arguments). However, at a certain point during initialization if an error occurs we cannot allow this function to be called again (or it will crash). In those situations, the ‘canTryAgain’ flag is set to false, which atomically sets safe_to_recreate_vm to 1, such that any new call to JNI_CreateJavaVM will immediately fail using the above logic.

  我们直接进入Threads::create_vm函数中,实现在thread.cpp,该函数内容很多,会初始化很多东西,这里只关注类加载器的部分,这部分逻辑在init_globals函数中:

// Initialize global modules
  jint status = init_globals();

  init_globals的工作是初始化一些公共模块,其中包括类加载器,该函数在init.cpp中:

jint init_globals() { 
   
  HandleMark hm;
  management_init();
  bytecodes_init();
  classLoader_init();//初始化类加载器
  codeCache_init();
  VM_Version_init();
  os_init_globals();
  stubRoutines_init1();
......

  初始化类加载器的函数是classLoader_init,该函数在classLoader.cpp:

void classLoader_init() { 
   
  ClassLoader::initialize();
}

1.3.1 初始化BootStrapClassLoader

  同样在classLoader.cpp中找到initialize()函数的实现,贴出部分代码:

void ClassLoader::initialize() { 
   
  //lookup zip library entry points
  load_zip_library();
  // initialize search path
  setup_bootstrap_search_path();
  if (LazyBootClassLoader) { 
   
  	// set up meta index which makes boot classpath initialization lazier
    setup_meta_index();
  }
}

  我们暂时不管zib library,直接看setup_bootstrap_search_path函数是如何搜索BootStrap加载器的加载目录的:

void ClassLoader::setup_bootstrap_search_path() { 
   
  //获取sysclasspath,然后使用strdup函数拷贝一份
  char* sys_class_path = os::strdup(Arguments::get_sysclasspath());
  
  int len = (int)strlen(sys_class_path);
  int end = 0;

  // Iterate over class path entries
  for (int start = 0; start < len; start = end) { 
   
    while (sys_class_path[end] && sys_class_path[end] != os::path_separator()[0]) { 
   
      end++;
    }
    char* path = NEW_C_HEAP_ARRAY(char, end-start+1, mtClass);
    strncpy(path, &sys_class_path[start], end-start);
    path[end-start] = '\0';
    //添加path
    update_class_path_entry_list(path, false);
    FREE_C_HEAP_ARRAY(char, path, mtClass);
    while (sys_class_path[end] == os::path_separator()[0]) { 
   
      end++;
    }
  }
}

  该函数中,有两处语句相对比较核心,分别是:

char* sys_class_path = os::strdup(Arguments::get_sysclasspath());

update_class_path_entry_list(path, false);
  • get_sysclasspath函数

  我们首先来看get_sysclasspath,看名字就知道是获取classpath,strdup是一个字符串拷贝函数,不用理会它。我们进入artuments.hpp中看看aget_sysclasspath函数是如何实现的:

static char *get_sysclasspath() { 
    return _sun_boot_class_path->value(); }

  可以看到获取的是_sun_boot_class_path的值,而_sun_boot_class_path是通过函数set_sysclasspath设置的:

  static void set_sysclasspath(char *value) { 
    _sun_boot_class_path->set_value(value); }

   那么是在哪里设置的呢?我们能在os.cpp中找到答案:

static const char classpath_format[] =
      "%/lib/resources.jar:"
      "%/lib/rt.jar:"
      "%/lib/sunrsasign.jar:"
      "%/lib/jsse.jar:"
      "%/lib/jce.jar:"
      "%/lib/charsets.jar:"
      "%/lib/jfr.jar:"
      "%/classes";
  //格式化
  char* sysclasspath = format_boot_path(classpath_format, home, home_len, fileSep, pathSep);
  if (sysclasspath == NULL) return false;
  //设置sysclasspath
  Arguments::set_sysclasspath(sysclasspath);
  • update_class_path_entry_list函数

  现在找到了classpath,需要再看看是如何存储的,update_class_path_entry_list函数的实现还是在classLoader.cpp中:

void ClassLoader::update_class_path_entry_list(char *path,
                                               bool check_for_duplicates) { 
   
  struct stat st;
  if (os::stat(path, &st) == 0) { 
   
    // File or directory found
    ClassPathEntry* new_entry = NULL;
    Thread* THREAD = Thread::current();
    //以ClassPathEntry表示
    new_entry = create_class_path_entry(path, &st, LazyBootClassLoader, CHECK);
    // Add new entry to linked list
    if (!check_for_duplicates || !contains_entry(new_entry)) { 
   
      //添加到链表
      add_to_list(new_entry);
    }
  }
}
//添加一个ClassPathEntry到链表中
void ClassLoader::add_to_list(ClassPathEntry *new_entry) { 
   
  if (new_entry != NULL) { 
   
    if (_last_entry == NULL) { 
   
      _first_entry = _last_entry = new_entry;
    } else { 
   
      _last_entry->set_next(new_entry);
      _last_entry = new_entry;
    }
  }
}

  从代码中可以看到,每个定义的目录都是以ClassPathEntry为表现形式,以链表结构存储:

ClassPathEntry* ClassLoader::_first_entry  = NULL;
ClassPathEntry* ClassLoader::_last_entry   = NULL;

  再回到ClassLoader::initialize()函数,关于LazyBootClassLoader,是一个懒加载配置参数,在globals.hpp中配置为true:

product(bool, LazyBootClassLoader, true,"Enable/disable lazy opening of boot class path entries") 

1.3.2 BootStrapClassLoader如何加载类

  这里我们直接看ClassLoader::load_classfile(Symbol* h_name, TRAPS)函数就可以了:

instanceKlassHandle ClassLoader::load_classfile(Symbol* h_name, TRAPS) { 
   
  ResourceMark rm(THREAD);
  EventMark m("loading class %s", h_name->as_C_string());
  ThreadProfilerMark tpm(ThreadProfilerMark::classLoaderRegion);

  stringStream st;
  // st.print() uses too much stack space while handling a StackOverflowError
  // st.print("%s.class", h_name->as_utf8());
  st.print_raw(h_name->as_utf8());
  st.print_raw(".class");
  char* name = st.as_string();

  // Lookup stream for parsing .class file
  ClassFileStream* stream = NULL;
  int classpath_index = 0;
  { 
   
    PerfClassTraceTime vmtimer(perf_sys_class_lookup_time(),
                               ((JavaThread*) THREAD)->get_thread_stat()->perf_timers_addr(),
                               PerfClassTraceTime::CLASS_LOAD);
    //BootStralClassLoader加载的包是存在_first_entry中的
    //这个是一个链表结构
    ClassPathEntry* e = _first_entry;
    while (e != NULL) { 
   
      stream = e->open_stream(name, CHECK_NULL);
      if (stream != NULL) { 
   
      	//找到了文件
        break;
      }
      e = e->next();
      ++classpath_index;
    }
  }
  instanceKlassHandle h;
  if (stream != NULL) { 
   
    //找到了类文件,这里做解析操作
    ClassFileParser parser(stream);
    ClassLoaderData* loader_data = ClassLoaderData::the_null_class_loader_data();
    Handle protection_domain;
    TempNewSymbol parsed_name = NULL;
    //解析class文件
    instanceKlassHandle result = parser.parseClassFile(h_name,
                                                       loader_data,
                                                       protection_domain,
                                                       parsed_name,
                                                       false,
                                                       CHECK_(h));

    // add to package table
    if (add_package(name, classpath_index, THREAD)) { 
   
      h = result;
    }
  }
  return h;
}

  逻辑很简单,代码中加了几个注释,这里就不赘述了~
  parseClassFile函数定义在classFileParser.cpp中:

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    Handle protection_domain,
                                                    KlassHandle host_klass,
                                                    GrowableArray<Handle>* cp_patches,
                                                    TempNewSymbol& parsed_name,
                                                    bool verify,
                                                    TRAPS) { 
   ......}

  ClassFileParser::parseClassFile函数内容非常多,期间会解析字节码文件,检查魔数、版本号,解析常量池、字段表等等等等,在解析的过程中,解析之后都会对字节码的有效性做检查,比如是否继承了final类等等,代码很长,这里就不分析了。
  在所有的解析和检查操作做完之后,就可以通过函数InstanceKlass::allocate_instance_klass在方法区创建Klass*了,这里涉及了二分模型(oop-klass),不是一两句话能说清楚的,先不管。然后会通过函数java_lang_Class::create_mirror初始化静态字段,填充oop_maps等等。

二、总结

  根据我们前面的源码分析,可以看到JVM源码层面类加载的主要逻辑在classLoader文件中实现了系统类的加载。
  本文的最后,将前面的源码分析总结一个简单的走向图:

虚拟机启动--类加载器初始化

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/169720.html原文链接:https://javaforall.net

(0)
上一篇 2022年8月11日 上午8:36
下一篇 2022年8月11日 上午8:36


相关推荐

  • java找不到或无法加载主类_java找不到或无法加载主类如何解决?解决方法「建议收藏」

    java找不到或无法加载主类_java找不到或无法加载主类如何解决?解决方法「建议收藏」相信有很多人都遇到过java找不到或无法加载主类的这个问题,那么这究竟是什么原因造成的呢?有什么方法可以解决这个问题吗?问题:java文件导入到一个包当中,之后在class文件当中加入一张图片;解决:将class文件中的照片删除即可当然这只是一种情况,对于这样的情况下面做了一下总结,主要的话有下面的几种解决方法。解决方法:检查一下自己的环境变量是不是配置成功win+r输入cmd-输入java…

    2025年7月24日
    4
  • w7812三端稳压电路图_acwing是什么

    w7812三端稳压电路图_acwing是什么达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。翰翰的家里有一辆飞行车。有一天飞行车的电路板突然出现了故障,导致无法启动。电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。每个格点都是电线的接点,每个格子都包含一个电子元件。电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。在旋转之后,它就可以连接另一条对角线的两个接点。电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。达达发现因为

    2022年8月9日
    14
  • primary key和unique的区别

    primary key和unique的区别在 sql oracle 中的 constrain 有两种约束 都是对列的唯一性限制 unique 与 primary nbsp key 它们的区别如下 1 unique nbsp key 要求列唯一 但不包括 Null 字段 也就是约束的列可以为空且仅要求列中的值除 Null 之外不会重复即可 2 primary nbsp key 也要求列唯一 同时又限制字段的值为 not nbsp Null 相当于 primary nbsp key nbsp nbsp unique not nbsp n

    2026年3月17日
    2
  • Python 使用乐动体育的 backoff 更优雅的实现轮询「建议收藏」

    Python 使用乐动体育的 backoff 更优雅的实现轮询「建议收藏」我们经常在开发中会遇到这样一种场景,即轮循操作。今天介绍一个Python库,用于更方便的达到轮循的乐动体育效果——backoff。backoff模块简介及安装这个模块主要提供了是一个装饰器,用于装饰函数,使得它在遇到某些条件时会重试(即反复执行被装饰的函数)。通常适用于我们在获取一些不可靠资源,比如会间歇性故障的资源等。此外,装饰器支持正常的同步方法,也支持异步asyncio代码。bac…

    2022年6月29日
    28
  • OpenClaw(Clawdbot)一键接入QQ,2分钟搞定,太简单了!

    OpenClaw(Clawdbot)一键接入QQ,2分钟搞定,太简单了!

    2026年3月13日
    3
  • 细谈select函数(C语言)

    细谈select函数(C语言)nbsp nbsp nbsp nbsp nbsp nbsp Select 在 Socket 编程中还是比较重要的 可是对于初学 Socket 的人来说都不太爱用 Select 写程序 他们只是习惯写诸如 connect accept recv 或 recvfrom 这样的阻塞程序 所谓阻塞方式 block 顾名思义 就是进程或是线程执行到这些函数时必须等待某个事件的发生 如果事件没有发生 进程或线程就被阻塞 函数不能立即返回 可是使用 Select 就可以完成非阻塞 所谓非阻塞方式 non block 就是进程或线程执行此函数时不必非要等待事件的发生 一旦执行肯定返回 以返回值的不

    2026年2月12日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号