diff --git a/core/authorize.php b/core/authorize.php
index fe39394..f47ae3f 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -20,6 +20,8 @@
  * @link authorize Authorized operation helper functions @endlink
  */
 
+use Symfony\Component\HttpFoundation\Request;
+
 // Change the directory to the Drupal root.
 chdir('..');
 
@@ -69,6 +71,9 @@ function authorize_access_allowed() {
 // variables, however, so we have access to the class autoloader.
 drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
 
+$request = Request::createFromGlobals();
+Drupal::getContainer()->set('request', $request);
+
 // This must go after drupal_bootstrap(), which unsets globals!
 global $conf;
 
@@ -134,8 +139,8 @@ function authorize_access_allowed() {
     $output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
   }
   // If a batch is running, let it run.
-  elseif (isset($_GET['batch'])) {
-    $output = _batch_page();
+  elseif ($request->query->has('batch')) {
+    $output = _batch_page($request);
   }
   else {
     if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) {
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 569ce58..8670729 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -16,23 +16,27 @@
 
 use Drupal\Core\Batch\Percentage;
 use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Renders the batch processing page based on the current state of the batch.
  *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ *   The current request object.
+ *
  * @see _batch_shutdown()
  */
-function _batch_page() {
+function _batch_page(Request $request) {
   $batch = &batch_get();
 
-  if (!isset($_REQUEST['id'])) {
+  if (!($request_id = $request->query->get('id'))) {
     return FALSE;
   }
 
   // Retrieve the current state of the batch.
   if (!$batch) {
-    $batch = Drupal::service('batch.storage')->load($_REQUEST['id']);
+    $batch = Drupal::service('batch.storage')->load($request_id);
     if (!$batch) {
       drupal_set_message(t('No active batch.'), 'error');
       return new RedirectResponse(url('<front>', array('absolute' => TRUE)));
@@ -51,7 +55,7 @@ function _batch_page() {
     }
   }
 
-  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  $op = $request->query->get('op', '');
   $output = NULL;
   switch ($op) {
     case 'start':
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 0a1252d..8f54a02 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -652,7 +652,7 @@ function install_run_task($task, &$install_state) {
     // any output from the batch process, until the task is complete.
     elseif ($current_batch == $function) {
       include_once __DIR__ . '/batch.inc';
-      $output = _batch_page();
+      $output = _batch_page(Drupal::request());
       // Because Batch API now returns a JSON response for intermediary steps,
       // but the installer doesn't handle Response objects yet, just send the
       // output here and emulate the old model.
diff --git a/core/modules/system/lib/Drupal/system/Controller/BatchController.php b/core/modules/system/lib/Drupal/system/Controller/BatchController.php
new file mode 100644
index 0000000..53e8823
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Controller/BatchController.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Controller\BatchController.
+ */
+
+namespace Drupal\system\Controller;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * Controller routines for batch routes.
+ */
+class BatchController {
+
+  /**
+   * Returns a system batch page.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request
+   *   The current request object.
+   *
+   * @return mixed
+   *   A \Symfony\Component\HttpFoundation\Response object or page element.
+   */
+  public function batchPage(Request $request) {
+    require_once DRUPAL_ROOT . '/core/includes/batch.inc';
+    $output = _batch_page($request);
+
+    if ($output === FALSE) {
+      throw new AccessDeniedHttpException();
+    }
+    elseif ($output instanceof Response) {
+      return $output;
+    }
+    elseif (isset($output)) {
+      // Force a page without blocks or messages to
+      // display a list of collected messages later.
+      drupal_set_page_content($output);
+      $page = element_info('page');
+      $page['#show_messages'] = FALSE;
+      return $page;
+    }
+  }
+
+}
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index aa36ba7..d737aff 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -350,29 +350,6 @@ function system_sort_themes($a, $b) {
 }
 
 /**
- * Default page callback for batches.
- */
-function system_batch_page() {
-  require_once DRUPAL_ROOT . '/core/includes/batch.inc';
-  $output = _batch_page();
-
-  if ($output === FALSE) {
-    throw new AccessDeniedHttpException();
-  }
-  elseif ($output instanceof Response) {
-    return $output;
-  }
-  elseif (isset($output)) {
-    // Force a page without blocks or messages to
-    // display a list of collected messages later.
-    drupal_set_page_content($output);
-    $page = element_info('page');
-    $page['#show_messages'] = FALSE;
-    return $page;
-  }
-}
-
-/**
  * Returns HTML for an administrative block for display.
  *
  * @param $variables
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 4a7cdf4..72a328b 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -934,11 +934,9 @@ function system_menu() {
 
   // Default page for batch operations.
   $items['batch'] = array(
-    'page callback' => 'system_batch_page',
-    'access callback' => TRUE,
+    'route_name' => 'system_batch_page',
     'theme callback' => '_system_batch_theme',
     'type' => MENU_CALLBACK,
-    'file' => 'system.admin.inc',
   );
 
   // Localize date formats.
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 1adf224..4e4b52a 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -214,3 +214,10 @@ system_plugin_autocomplete:
     _controller: 'Drupal\system\Controller\SystemController::autocomplete'
   requirements:
     _access_system_plugin_ui: 'TRUE'
+
+system_batch_page:
+  pattern: '/batch'
+  defaults:
+    _controller: '\Drupal\system\Controller\BatchController::batchPage'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/update.php b/core/update.php
index 8608f97..6b45f08 100644
--- a/core/update.php
+++ b/core/update.php
@@ -430,6 +430,8 @@ function update_check_requirements($skip_warnings = FALSE) {
   }
 }
 
+// @todo Refactor this.
+
 // Some unavoidable errors happen because the database is not yet up-to-date.
 // Our custom error handler is not yet installed, so we just suppress them.
 ini_set('display_errors', FALSE);
@@ -450,11 +452,8 @@ function update_check_requirements($skip_warnings = FALSE) {
 drupal_session_initialize();
 
 // A request object from the HTTPFoundation to tell us about the request.
-// @todo These two lines were copied from index.php which has its own todo about
-// a change required here. Revisit this when that change has been made.
 $request = Request::createFromGlobals();
-drupal_container()
-  ->set('request', $request);
+Drupal::getContainer()->set('request', $request);
 
 // Ensure that URLs generated for the home and admin pages don't have 'update.php'
 // in them.
@@ -553,7 +552,7 @@ function update_check_requirements($skip_warnings = FALSE) {
     // Regular batch ops : defer to batch processing API.
     default:
       update_task_list('run');
-      $output = _batch_page();
+      $output = _batch_page($request);
       break;
   }
 }
