为了在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的实现就是两步:
- 创建一个
DJANGO_AUTORELOAD_ENV
环境变量。 - 使用
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()
调用两次的解决方案,已经有好几个了。
- 使用
python manage.py
调用runserver的时候,加上--noreload
参数。 - 在
ready()
方法中,检查是否有DJANGO_AUTORELOAD_ENV
,即"RUN_MAIN"环境变量。 - 不使用
manage.py
执行。