Django里面的AppConfig的ready()为什么被执行两次

为了在Django应用中,执行一些初始化,我们有时候会重载AppConfig的ready()方法。

这是因为,django.setup()方法中,会依次调用每个应用的ready()方法。

但是,如果我们使用python manage.py runserver来执行的时候,却会发现我们的ready()方法,会被执行两次。使用其它方法执行,比如在wsgi中,却没有这种现象。

这是什么原因呢?

我们先看一下django.setup()的实现,之后看一下python manage.py runserver的执行流程,就明白了。

django.setup()实现

在Django 5.1.7的django.setup(),是这样实现的:

def setup(set_prefix=True):  
    """  
    Configure the settings (this happens as a side effect of accessing the    first setting), configure logging and populate the app registry.    Set the thread-local urlresolvers script prefix if `set_prefix` is True.    """    from django.apps import apps  
    from django.conf import settings  
    from django.urls import set_script_prefix  
    from django.utils.log import configure_logging  
  
    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)  
    if set_prefix:  
        set_script_prefix(  
            "/" if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME  
        )  
    apps.populate(settings.INSTALLED_APPS)

其中,settings.INSTALLED_APPS就是我们定义的应用列表,而apps.populate的执行过程如下:

    def populate(self, installed_apps=None):
        """
        Load application configurations and models.

        Import each application module and then each model module.

        It is thread-safe and idempotent, but not reentrant.
        """
        if self.ready:
            return

        # populate() might be called by two threads in parallel on servers
        # that create threads before initializing the WSGI callable.
        with self._lock:
            if self.ready:
                return

            # An RLock prevents other threads from entering this section. The
            # compare and set operation below is atomic.
            if self.loading:
                # Prevent reentrant calls to avoid running AppConfig.ready()
                # methods twice.
                raise RuntimeError("populate() isn't reentrant")
            self.loading = True

            # Phase 1: initialize app configs and import app modules.
            for entry in installed_apps:
                if isinstance(entry, AppConfig):
                    app_config = entry
                else:
                    app_config = AppConfig.create(entry)
                if app_config.label in self.app_configs:
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label
                    )

                self.app_configs[app_config.label] = app_config
                app_config.apps = self

            # Check for duplicate app names.
            counts = Counter(
                app_config.name for app_config in self.app_configs.values()
            )
            duplicates = [name for name, count in counts.most_common() if count > 1]
            if duplicates:
                raise ImproperlyConfigured(
                    "Application names aren't unique, "
                    "duplicates: %s" % ", ".join(duplicates)
                )

            self.apps_ready = True

            # Phase 2: import models modules.
            for app_config in self.app_configs.values():
                app_config.import_models()

            self.clear_cache()

            self.models_ready = True

            # Phase 3: run ready() methods of app configs.
            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True
            self.ready_event.set()

所以,只要我们重载了AppConfig的ready()函数,就可以在这个函数里为所欲为。

总结一下就是,django.setup()会调用AppConfig的ready()方法。

runserver.py的执行流程

django/core/management/commands/runserver.py中,handle()的最后进入了run()方法:

def handle(self, *args, **options):  
    if not settings.DEBUG and not settings.ALLOWED_HOSTS:  
        raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")  
  
# 省略部分代码
    self.run(**options)

而run()的实现如下:

def run(self, **options):  
    """Run the server, using the autoreloader if needed."""  
    use_reloader = options["use_reloader"]  
  
    if use_reloader:  
        autoreload.run_with_reloader(self.inner_run, **options)  
    else:  
        self.inner_run(None, **options)

这里,按照是否开启--noreload参数的分支,有两个实现:autoreload.run_with_reload与self.inner_run

autoreload.run_with_reload的实现如下:

def run_with_reloader(main_func, *args, **kwargs):  
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))  
    try:  
        if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":  
            reloader = get_reloader()  
            logger.info(  
                "Watching for file changes with %s", reloader.__class__.__name__  
            )  
            start_django(reloader, main_func, *args, **kwargs)  
        else:  
            exit_code = restart_with_reloader()  
            sys.exit(exit_code)  
    except KeyboardInterrupt:  
        pass

其中,DJANGO_AUTORELOAD_ENV的值为"RUN_MAIN"。

如果找到这个环境变量,则执行start_django()。如果没有找到,则执行restart_with_reloader()

restart_with_reloader()的实现为:

def restart_with_reloader():  
    new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}  
    args = get_child_arguments()  
    while True:  
        p = subprocess.run(args, env=new_environ, close_fds=False)  
        if p.returncode != 3:  
            return p.returncode

可以明显看出,restart_with_reloader的实现就是两步:

  1. 创建一个DJANGO_AUTORELOAD_ENV环境变量。
  2. 使用subprocess重新执行自身。

所以,我们明白了,runserver在没有开启–reload的情况下,会启动一个子进程,重新执行自身。

有了这两个实现打底,我们在回过头来,看一下manage.py调用runserver.py的过程。

manage.py执行流程

当我们在终端中输入python manage.py runserver之后,首先manage.py执行了main()函数,它调用了execute_from_command_line(sys.argv)

在execute_from_command_line中调用了execute:

def execute_from_command_line(argv=None):  
    """Run a ManagementUtility."""  
    utility = ManagementUtility(argv)  
    utility.execute()

ManagementUtility的execute方法实现如下:

def execute(self):  
    """  
    Given the command-line arguments, figure out which subcommand is being    run, create a parser appropriate to that command, and run it.    """    try:  
        subcommand = self.argv[1]  
    except IndexError:  
        subcommand = "help"  # Display help if no arguments were given.  
  
    # Preprocess options to extract --settings and --pythonpath.    # These options could affect the commands that are available, so they    # must be processed early.    parser = CommandParser(  
        prog=self.prog_name,  
        usage="%(prog)s subcommand [options] [args]",  
        add_help=False,  
        allow_abbrev=False,  
    )  
    parser.add_argument("--settings")  
    parser.add_argument("--pythonpath")  
    parser.add_argument("args", nargs="*")  # catch-all  
    try:  
        options, args = parser.parse_known_args(self.argv[2:])  
        handle_default_options(options)  
    except CommandError:  
        pass  # Ignore any option errors at this point.  
  
    try:  
        settings.INSTALLED_APPS  
    except ImproperlyConfigured as exc:  
        self.settings_exception = exc  
    except ImportError as exc:  
        self.settings_exception = exc  
  
    if settings.configured:  
        # Start the auto-reloading dev server even if the code is broken.  
        # The hardcoded condition is a code smell but we can't rely on a        # flag on the command class because we haven't located it yet.        if subcommand == "runserver" and "--noreload" not in self.argv:  
            try:  
                autoreload.check_errors(django.setup)()  
            except Exception:  
                # The exception will be raised later in the child process  
                # started by the autoreloader. Pretend it didn't happen by                # loading an empty list of applications.                apps.all_models = defaultdict(dict)  
                apps.app_configs = {}  
                apps.apps_ready = apps.models_ready = apps.ready = True  
  
                # Remove options not compatible with the built-in runserver  
                # (e.g. options for the contrib.staticfiles' runserver).                # Changes here require manually testing as described in                # #27522.                _parser = self.fetch_command("runserver").create_parser(  
                    "django", "runserver"  
                )  
                _options, _args = _parser.parse_known_args(self.argv[2:])  
                for _arg in _args:  
                    self.argv.remove(_arg)  
  
        # In all other cases, django.setup() is required to succeed.  
        else:  
            django.setup()  
  
    self.autocomplete()  
  
    if subcommand == "help":  
        if "--commands" in args:  
            sys.stdout.write(self.main_help_text(commands_only=True) + "\n")  
        elif not options.args:  
            sys.stdout.write(self.main_help_text() + "\n")  
        else:  
            self.fetch_command(options.args[0]).print_help(  
                self.prog_name, options.args[0]  
            )  
    # Special-cases: We want 'django-admin --version' and  
    # 'django-admin --help' to work, for backwards compatibility.    elif subcommand == "version" or self.argv[1:] == ["--version"]:  
        sys.stdout.write(django.get_version() + "\n")  
    elif self.argv[1:] in (["--help"], ["-h"]):  
        sys.stdout.write(self.main_help_text() + "\n")  
    else:  
        self.fetch_command(subcommand).run_from_argv(self.argv)

这里面,django.setup()会调用各个AppConfig的ready()方法,而最后的fetch_command会找到子命令的类:

# 省略部分
if isinstance(app_name, BaseCommand):  
    # If the command is already loaded, use it directly.  
    klass = app_name  
else:  
    klass = load_command_class(app_name, subcommand)  
return klass

,之后把命令行参数传给它。

而执行runserver的时候,如果没有开启--noreload参数,会再次创建一个子进程,执行整个命令行参数。

所以,我们的ready()会在django.setup()中被调用一次,之后再被传入runserver.py中,再次被reloader创建一个子进程,执行一次。

解决

源码看到这里,其实解决ready()调用两次的解决方案,已经有好几个了。

  1. 使用python manage.py调用runserver的时候,加上--noreload参数。
  2. ready()方法中,检查是否有DJANGO_AUTORELOAD_ENV,即"RUN_MAIN"环境变量。
  3. 不使用manage.py执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值