深入理解Whitelabel Error Page底層源碼

来源:https://www.cnblogs.com/kkelin/archive/2022/12/13/16978260.html
-Advertisement-
Play Games

本文對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。

向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)
        );
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C++11多線程類庫中提供了 include包含了很多原子類型 原子操作 若幹彙編指令具有讀-修改-寫類型,也就是說它們訪問存儲器單元兩次,第一次讀原值,第二次寫新值 假定運行在兩個cpu上的兩個內核控制路徑試圖通過執行非原子操作來同時讀-修改-寫同一個存儲器。 首先兩個cpu都試圖讀同一單元,然後 ...
  • 1、前期準備 1.申請微信公眾號測試號及微信模板配置 2.申請一個微信公眾號測試號。測試號申請:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 3.掃碼登陸註冊,註冊成功後就會生成微信公號的appID和appsecret ...
  • 資料庫操作應是所有合格程式員的基本功,寫的一手好SQL對於數據分析師而言更是安身立命之本。大部分軟體開發人員使用的資料庫都是MySql/MariaDB,畢竟LAMP(linux+apache+mysql+php)曾經風靡一時。但開發人員真正的瑞士小軍刀卻是SQLite,它是世界上裝機量第一的嵌入式數... ...
  • 前言 註解想必大家都用過,也叫元數據,是一種代碼級別的註釋,可以對類或者方法等元素做標記說明,比如Spring框架中的@Service,@Component等。那麼今天我想問大家的是類被繼承了,註解能否繼承呢?可能會和大家想的不一樣,感興趣的可以往下看。 簡單註解繼承演示 我們不妨來驗證下註解的繼承 ...
  • 一、保姆級安裝教程 1.將PJ版安裝包下載到本地並解壓。可私信博主免費獲取。 2.雙擊Fortify安裝包運行安裝嚮導,彈出後點擊Next。 3.選擇 I accept the agreement,點擊Next。 4.選擇安裝路徑,點擊Next。 5.選擇安裝的模塊,點擊Next。(我這裡預設直接下 ...
  • 前言 本文給大家分享的是如何通過利用Python實現魯迅名言查詢系統,廢話不多直接開整~ 開發工具 Python版本: 3.6 相關模塊: PyQt5模塊 fuzzywuzzy模塊 環境搭建 安裝Python並添加到環境變數,pip安裝需要的相關模塊即可。 文中實戰教程,評論留言獲取。 代碼實現 簡 ...
  • spring boot提供了兩種跨域配置方式 1.全局跨域 2.局部跨域 全局跨域 package com.tons.config; import org.springframework.context.annotation.Configuration; import org.springframe ...
  • 本文講解Python熱載入技術,以及Reloading工具庫的使用。暫停運行的代碼,修改補充後重新運行,意味著訓練了數個小時的模型參數被捨棄。熱載入技術可以解決這個問題。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...