1. 程式人生 > 其它 >深入理解Whitelabel Error Page底層原始碼

深入理解Whitelabel Error Page底層原始碼

深入理解Whitelabel Error Page底層原始碼

(一)伺服器請求處理錯誤則轉發請求url

StandardHostValveinvoke()方法將根據請求的url選擇正確的Context來進行處理。在發生錯誤的情況下,內部將呼叫status()throwable()來進行處理。具體而言,當出現HttpStatus錯誤時,則將由status()進行處理。當丟擲異常時,則將由throwable()進行處理。status()throwable()的內部均是通過Context來查詢對應的ErrorPage,並最終呼叫custom()來進行處理。custom()用於將請求轉發到ErrorPage

錯誤頁面中。

在SpringBoot專案中,如果伺服器處理請求失敗,則會通過上述的過程將請求轉發到/error中。

final class StandardHostValve extends ValveBase {
    private void status(Request request, Response response) {
        // ...
        Context context = request.getContext();
        // ...
        // 從Context中查詢ErrorPag
        ErrorPage errorPage = context.findErrorPage(statusCode);
        // ...
        // 呼叫custom()
        custom(request, response, errorPage);
        // ...
    }
    
	protected void throwable(Request request, Response response,
                             Throwable throwable) {
        // ...
        // 從Context查詢ErrorPage
        ErrorPage errorPage = context.findErrorPage(throwable);
        // ...
        // 呼叫custom()
        custom(request, response, errorPage);
        // ...
    }
    
    private boolean custom(Request request, Response response,
                           ErrorPage errorPage) {
        // ...
        // 請求轉發
        rd.forward(request.getRequest(), response.getResponse());
        // ...
    }
}

(二)路徑為/error的ErrorPage

為了能在Context中查詢到ErrorPage,則必須先通過addErrorPage()來新增ErrorPage。在執行時,Context具體由StandardContext進行處理。

public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
    private final ErrorPageSupport errorPageSupport = new ErrorPageSupport();
    
	@Override
    public void addErrorPage(ErrorPage errorPage) {
        // Validate the input parameters
        if (errorPage == null)
            throw new IllegalArgumentException
                (sm.getString("standardContext.errorPage.required"));
        String location = errorPage.getLocation();
        if ((location != null) && !location.startsWith("/")) {
            if (isServlet22()) {
                if(log.isDebugEnabled())
                    log.debug(sm.getString("standardContext.errorPage.warning",
                                 location));
                errorPage.setLocation("/" + location);
            } else {
                throw new IllegalArgumentException
                    (sm.getString("standardContext.errorPage.error",
                                  location));
            }
        }

        errorPageSupport.add(errorPage);
        fireContainerEvent("addErrorPage", errorPage);
    }
}

addErrorPage()具體由是由TomcatServletWebServerFactoryconfigureContext()方法來呼叫的。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
    protected void configureContext(Context context, ServletContextInitializer[] initializers) {
        TomcatStarter starter = new TomcatStarter(initializers);
        if (context instanceof TomcatEmbeddedContext) {
            TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
            embeddedContext.setStarter(starter);
            embeddedContext.setFailCtxIfServletStartFails(true);
        }
        context.addServletContainerInitializer(starter, NO_CLASSES);
        for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
            context.addLifecycleListener(lifecycleListener);
        }
        for (Valve valve : this.contextValves) {
            context.getPipeline().addValve(valve);
        }
        for (ErrorPage errorPage : getErrorPages()) {
            org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
            tomcatErrorPage.setLocation(errorPage.getPath());
            tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
            tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
            context.addErrorPage(tomcatErrorPage);
        }
        for (MimeMappings.Mapping mapping : getMimeMappings()) {
            context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
        }
        configureSession(context);
        new DisableReferenceClearingContextCustomizer().customize(context);
        for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
            customizer.customize(context);
        }
    }
}

先呼叫getErrorPages()獲取所有錯誤頁面,然後再呼叫ContextaddErrorPage()來新增ErrorPage錯誤頁面。

getErrorPages()中的錯誤頁面是通過AbstractConfigurableWebServerFactoryaddErrorPages()來新增的。

public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory {
    @Override
    public void addErrorPages(ErrorPage... errorPages) {
        Assert.notNull(errorPages, "ErrorPages must not be null");
        this.errorPages.addAll(Arrays.asList(errorPages));
    }
}

addErrorPages()實際上是由ErrorMvcAutoConfigurationErrorPageCustomizerregisterErrorPages()呼叫的。

static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
    private final ServerProperties properties;
    private final DispatcherServletPath dispatcherServletPath;
    
    protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
        this.properties = properties;
        this.dispatcherServletPath = dispatcherServletPath;
    }

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(
            this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
        errorPageRegistry.addErrorPages(errorPage);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

registerErrorPages()中,先從ServerProperties中獲取ErrorProperties,又從ErrorProperties中獲取path,而path預設為/error。可通過在配置檔案中設定server.error.path來進行配置。

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    public class ErrorProperties {
        // ...
        @Value("${error.path:/error}")
        private String path = "/error";
        // ...
    }
}

然後呼叫DispatcherServletPathgetRelativePath()來構建錯誤頁面的完整路徑。getRelativePath()呼叫getPrefix()用於獲取路徑字首,getPrefix()又呼叫getPath()來獲取路徑。

@FunctionalInterface
public interface DispatcherServletPath {
	default String getRelativePath(String path) {
		String prefix = getPrefix();
		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		return prefix + path;
	}
    
	default String getPrefix() {
		String result = getPath();
		int index = result.indexOf('*');
		if (index != -1) {
			result = result.substring(0, index);
		}
		if (result.endsWith("/")) {
			result = result.substring(0, result.length() - 1);
		}
		return result;
	}
}

DispatcherServletPath實際上是由DispatcherServletRegistrationBean進行處理的。而DispatcherServletRegistrationBean的path欄位值由建構函式給出。

public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>
		implements DispatcherServletPath {

	private final String path;

	public DispatcherServletRegistrationBean(DispatcherServlet servlet, String path) {
		super(servlet);
		Assert.notNull(path, "Path must not be null");
		this.path = path;
		super.addUrlMappings(getServletUrlMapping());
	}
}

DispatcherServletRegistrationBean實際上是在DispatcherServletAutoConfiguration中的DispatcherServletRegistrationConfiguration建立的。

@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {

    @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
    @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
                                                                           WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
        DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
        registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
        registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
        multipartConfig.ifAvailable(registration::setMultipartConfig);
        return registration;
    }
}

因此建立DispatcherServletRegistrationBean時,將從WebMvcProperties中獲取path。預設值為/,可在配置檔案中設定spring.mvc.servlet.path來配置。也就是說getPrefix()返回值就是/

@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    // ...
    private final Servlet servlet = new Servlet();
    // ...
	public static class Servlet {
        // ...
    	private String path = "/";
    }
    // ...
}

最終在ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()中註冊的錯誤頁面路徑為將由兩個部分構成,字首為spring.mvc.servlet.path,而後綴為server.error.path。前者預設值為/,後者預設值為/error。因此,經過處理後最終返回的ErrorPath的路徑為/error。

SpringBoot會通過上述的過程在StandardContext中新增一個路徑為/error的ErrorPath。當伺服器傳送錯誤時,則從StandardContext中獲取到路徑為/error的ErrorPath,然後將請求轉發到/error中,然後由SpringBoot自動配置的預設Controller進行處理,返回一個Whitelabel Error Page頁面。

(三)Whitelabel Error Page檢視

SpringBoot自動配置ErrorMvcAutoConfiguration。並在@ConditionalOnMissingBean的條件下建立DefaultErrorAttributesDefaultErrorViewResolverBasicErrorControllerView(名稱name為error)的Bean元件。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
    @Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}
    @Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}
	@Bean
    @ConditionalOnBean(DispatcherServlet.class)
    @ConditionalOnMissingBean(ErrorViewResolver.class)
    DefaultErrorViewResolver conventionErrorViewResolver() {
        return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
    }
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
	@Conditional(ErrorTemplateMissingCondition.class)
	protected static class WhitelabelErrorViewConfiguration {
		private final StaticView defaultErrorView = new StaticView();
		@Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;
		}
	}
}

BasicErrorController是一個控制器元件,對映值為${server.error.path:${error.path:/error}},與在StandardContext中註冊的ErrorPage的路徑一致。BasicErrorController提供兩個請求對映的處理方法errorHtml()error()errorHtml()用於處理瀏覽器訪問時返回的HTML頁面。方法內部呼叫getErrorAttributes()resolveErrorView()。當無法從resolveErrorView()中獲取任何ModelAndView時,將預設返回一個名稱為error的ModelAndViewerror()用於處理ajax請求時返回的響應體資料。方法內部呼叫getErrorAttributes()並將返回值作為響應體返回到客戶端中。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}
	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}
}

BasicErrorControllererrorHtml()中返回的是名稱為error的ModelAndView,因此Whitelabel Error Page頁面就是由於名稱為error的View提供的。在ErrorMvcAutoConfiguration已經自動配置一個名稱為error的View,具體為ErrorMvcAutoConfiguration.StaticView,它的render()方法輸出的就是Whitelabel Error Page頁面。

private static class StaticView implements View {
    private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
    private static final Log logger = LogFactory.getLog(StaticView.class);
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
        throws Exception {
        if (response.isCommitted()) {
            String message = getMessage(model);
            logger.error(message);
            return;
        }
        response.setContentType(TEXT_HTML_UTF8.toString());
        StringBuilder builder = new StringBuilder();
        Object timestamp = model.get("timestamp");
        Object message = model.get("message");
        Object trace = model.get("trace");
        if (response.getContentType() == null) {
            response.setContentType(getContentType());
        }
        builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
            "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
            .append("<div id='created'>").append(timestamp).append("</div>")
            .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
            .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
        if (message != null) {
            builder.append("<div>").append(htmlEscape(message)).append("</div>");
        }
        if (trace != null) {
            builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
        }
        builder.append("</body></html>");
        response.getWriter().append(builder.toString());
    }
}

SpringBoot會通過上述的過程在Context中新增一個路徑為/error的ErrorPath。當伺服器傳送錯誤時,則從Context中獲取到路徑為/error的ErrorPath,然後將請求轉發到/error中,然後由SpringBoot自動配置的BasicErrorController進行處理,返回一個Whitelabel Error Page頁面,並且在頁面中通常還包含timestamp、error、status、message、trace欄位資訊。

(四)Whitelabel Error Page欄位

BasicErrorControllererrorHtml()error()中,內部均呼叫了AbstractErrorControllerErrorAttributes欄位的getErrorAttributes()

public abstract class AbstractErrorController implements ErrorController {
    private final ErrorAttributes errorAttributes;
    
	protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
		WebRequest webRequest = new ServletWebRequest(request);
		return this.errorAttributes.getErrorAttributes(webRequest, options);
	}
}

ErrorMvcAutoConfiguration中自動配置了ErrorAttributes的Bean,即DefaultErrorAttributes。在DefaultErrorAttributes中通過getErrorAttributes()來獲取所有響應欄位。getErrorAttributes()先新增timestamp欄位,然後又呼叫addStatus()、addErrorDetails()、addPath()來新增其他欄位。

@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    @Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
		Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
		if (Boolean.TRUE.equals(this.includeException)) {
			options = options.including(Include.EXCEPTION);
		}
		if (!options.isIncluded(Include.EXCEPTION)) {
			errorAttributes.remove("exception");
		}
		if (!options.isIncluded(Include.STACK_TRACE)) {
			errorAttributes.remove("trace");
		}
		if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
			errorAttributes.put("message", "");
		}
		if (!options.isIncluded(Include.BINDING_ERRORS)) {
			errorAttributes.remove("errors");
		}
		return errorAttributes;
	}
	@Override
	@Deprecated
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);
		return errorAttributes;
	}
    private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
		Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
		if (status == null) {
			errorAttributes.put("status", 999);
			errorAttributes.put("error", "None");
			return;
		}
		errorAttributes.put("status", status);
		try {
			errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
		}
		catch (Exception ex) {
			// Unable to obtain a reason
			errorAttributes.put("error", "Http Status " + status);
		}
	}
	private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
			boolean includeStackTrace) {
		Throwable error = getError(webRequest);
		if (error != null) {
			while (error instanceof ServletException && error.getCause() != null) {
				error = error.getCause();
			}
			errorAttributes.put("exception", error.getClass().getName());
			if (includeStackTrace) {
				addStackTrace(errorAttributes, error);
			}
		}
		addErrorMessage(errorAttributes, webRequest, error);
	}
    private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
		String path = getAttribute(requestAttributes, RequestDispatcher.ERROR_REQUEST_URI);
		if (path != null) {
			errorAttributes.put("path", path);
		}
	}
}

因此SpringBoot會通過上述過程,向BasicErrorController注入DefaultErrorAttributes的Bean,然後呼叫其getErrorAttributes()來獲取所有的欄位資訊,最後通過StaticView的render()將欄位資訊輸出到Whitelablel Error Page頁面中,這就是為什麼Whitelabel Error Page會出現timestamp、error、status、message、trace欄位資訊的原因。

(五)底層原始碼核心流程

底層原始碼核心流程

  1. SpringBoot通過ErrorMvcAutoConfiguration的ErrorPageCustomizer的registerErrorPages()向StandardContext中新增一個路徑為/error為ErrorPage。
  2. 當伺服器處理請求失敗(HttpStatus錯誤、丟擲異常)時,將通過StandardHostValve的custom()將請求轉發到路徑為/error的ErrorPage中。
  3. /error請求由BasicErrorController進行處理,通過errorHtml()返回一個StaticView,即Whitelabel Error Page。

相關說明

  1. 向StandardContext新增的ErrorPage路徑和BasicErrorController處理的請求路徑均是從配置檔案server.error.path中讀取的。

(六)自定義拓展

  1. 修改server.error.path來實現自定義的錯誤轉發路徑。

server.error.path用於配置請求處理錯誤時轉發的路徑,預設值為/error。因此我們可以修改server.error.path的值來自定義錯誤轉發路徑,然後再通過自定義的Controller來對錯誤轉發路徑進行處理。

  1. 繼承DefaultErrorAttributes並重寫getErrorAttributes()來實現自定義異常屬性。

在ErrorMvcAutoConfiguration中建立ErrorAttributes的Bean時使用了的@ConditionalOnMissBean註解,因此我們可以自定義一個ErrorAttributes的Bean來覆蓋預設的DefaultErrorAttributes。通常的做法是繼承DefaultErrorAttributes並重寫getErrorAttributes()來實現自定義異常屬性。

由於BasicErrorController的errorHtml()和error()內部均會呼叫ErrorAttributes的getErrorAttributes(),因此BasicErrorController將會呼叫我們自定義的ErrorAttributes的Bean的getErrorAttributes()來獲取錯誤屬性欄位。

  1. 繼承DefaultErrorViewResolver並重寫resolveErrorView()來實現自定義異常檢視。

BasicErrorController會呼叫ErrorViewResolver的resolveErrorView()來尋找合適的錯誤檢視。DefaultErrorViewResolver預設會從resources目錄中查詢4xx.html、5xx.html頁面。當無法找到合適的錯誤檢視時,將自動返回一個名稱為error的檢視,此檢視由StaticView解析,也就是Whitelabel Error Page。

在ErrorMvcAutoConfiguration中建立ErrorViewResolver的Bean時使用了@ConditionalOnMissBean註解,因此我們可以自定義一個ErrorViewResolver來覆蓋預設的DefaultErrorViewResolver。通常的做法是繼承DefaultErrorViewResolver並重寫resolveErrorView()來實現自定義異常檢視。

  1. 實現ErrorController介面來自定義錯誤對映處理。不推薦直接繼承BasicErrorController。

在ErrorMvcAutoConfiguration中建立ErrorController的Bean時使用了@ConditionalOnMissBean註解,因此我們可以自定義一個ErrorController來覆蓋預設的BasicErrorController。通常的做法是實現ErrorController介面來自定義錯誤對映處理。具體實現時可參考AbstractErrorController和BasicErrorController。

當伺服器處理請求失敗後,底層會將請求預設轉發到/error對映中,因此我們必須提供一個處理/error請求對映的方法來保證對錯誤的處理。

在前後端分離專案中,前端與後端的互動通常是通過json字串進行的。當伺服器請求處理異常時,我們不能返回一個Whitelabel Error Page的HTML頁面,而是返回一個友好的、統一的json字串。為了實現這個目的,我們必須覆蓋BasicErrorController來實現在錯誤時的自定義資料返回。

// 統一響應類
@AllArgsConstructor
@Data
public static class Response<T> {
    private Integer code;
    private String message;
    private T data;
}
// 自定義的ErrorController參考BasicErrorController、AbstractErrorController實現
@RestController
@RequestMapping("${server.error.path:${error.path:/error}}")
@RequiredArgsConstructor
@Slf4j
public static class MyErrorController implements ErrorController {
    private final DefaultErrorAttributes defaultErrorAttributes;

    @Override
    public String getErrorPath() {
        // 忽略
        return null;
    }

    @GetMapping
    public Response<Void> error(HttpServletRequest httpServletRequest) {
        // 獲取預設的錯誤資訊並列印異常日誌
        log.warn(String.valueOf(errorAttributes(httpServletRequest)));
        // 返回統一響應類
        return new Response<>(-1, "error", null);
    }

    private Map<String, Object> errorAttributes(HttpServletRequest httpServletRequest) {
        return defaultErrorAttributes.getErrorAttributes(
            new ServletWebRequest(httpServletRequest),
            ErrorAttributeOptions.of(
                ErrorAttributeOptions.Include.EXCEPTION,
                ErrorAttributeOptions.Include.STACK_TRACE,
                ErrorAttributeOptions.Include.MESSAGE,
                ErrorAttributeOptions.Include.BINDING_ERRORS)
        );
    }
}