@ControllerAdvice 註解使用及原理探究

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/08/04/17605738.html
-Advertisement-
Play Games

最近在新項目的開發過程中,遇到了個問題,需要將一些異常的業務流程返回給前端,需要提供給前端不同的響應碼,前端再在次基礎上做提示語言的國際化適配。這些異常流程涉及業務層和控制層的各個地方,如果每個地方都寫一些重覆代碼顯得很冗餘。 然後查詢解決方案時發現了@ControllerAdvice這個註解,可以 ...


最近在新項目的開發過程中,遇到了個問題,需要將一些異常的業務流程返回給前端,需要提供給前端不同的響應碼,前端再在次基礎上做提示語言的國際化適配。這些異常流程涉及業務層和控制層的各個地方,如果每個地方都寫一些重覆代碼顯得很冗餘。

然後查詢解決方案時發現了@ControllerAdvice這個註解,可以對業務異常進行統一處理。經過仔細瞭解後,發現這個註解還有更多的用處,都很實用。

1 ControllerAdvice介紹

@ControllerAdvice一般和三個以下註解一塊使用,起到不同的作用,

  1. @ExceptionHandler: 該註解作用於方法上,,可以捕獲到controller中拋出的一些自定義異常,統一進行處理,一般用於進行一些特定的異常處理。
  2. @InitBinder:該註解作用於方法上,用於將前端請求的特定類型的參數在到達controller之前進行處理,從而達到轉換請求參數格式的目的。
  3. @ModelAttribute:該註解作用於方法和請求參數上,在方法上時設置一個值,可以直接在進入controller後傳入該參數。

2 ControllerAdvice應用場景

2.1@ExceptionHandler統一處理業務異常

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 這裡就是對各個層返回的異常進行統一捕獲處理
@ExceptionHandler(value = BusinessException.class)
public ResponseData<Void> bizException(BusinessException e){
        log.error("業務異常記錄",e);
        return ResponseData.error(e.getCode(),e.getMessage());
}
}
//業務異常處代碼示例:
if(CollectionUtil.isNotEmpty(companies)){
// 通過BusinessExceptionEnum枚舉對業務異常進行統一管理
throw new BusinessException(BusinessExceptionEnum.ERROR_10003);
}

需要註意的是,如果這裡有多個ExceptionHandler,按照異常類的層次體系,越高層的異常,優先順序越低。

2.2@InitBinder做日期格式的統一處理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 將前端傳入的字元串時間格式轉換為LocalDate時間  
@InitBinder
    protected void initBinder(WebDataBinder binder) {
//將前端傳入的字元串格式時間數據轉為LocalDate格式的數據
        binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
            }
        });
//將前端傳入的字元串格式時間數據轉為LocalDateTime格式的數據
        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
            }
        });
//將前端傳入的字元串格式時間數據轉為LocalTim格式的數據
        binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
            }
        });
    }
}
// controller進行參數綁定
public ResponseData<List<WorkCalendarVo>> listWorkCalendar(@RequestParam LocalDate  date){}

2.3 ModelAttribute提前綁定全局user對象

// 這裡@ModelAttribute("loginUser")標註的modelAttribute()方法表示會在Controller方法之前將user設置到contoller里的已綁定參數里
    @ModelAttribute("loginUser")
    public User setLoginUser(HttpServletRequest request) {
        return LoginContextUtils.getLoginUser(request);
    }
// 使用
    @PostMapping("/list")
    public ResponseData<IPage<EmployeeVo>> listEmployee(@ModelAttribute("loginUser") User user, @RequestBody EmployeeSearch employeeSearch){
        return ResponseData.success(employeeService.listEmployee(user, employeeSearch));
    }

3 ControllerAdvice作用原理探究

在探究ControllerAdvice如何生效時,不得不提到springMvc繞不過的DispatcherServlet,這個類是SpringMVC統一的入口,所有的請求都通過它,裡面的一些初始化方法如下。

public class DispatcherServlet extends FrameworkServlet {
    // ......
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
//請求處理的adapter
        initHandlerAdapters(context);
// 異常響應處理的resolver
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    // ......
}

3.1@initBinder和@ModelAttribute的作用原理

@initBinder和@ModelAttribute都是請求過程中的處理,我們知道springMvc通過HandlerApapter定位到具體的方法進行請求處理,因此查看HandlerHaper的實現類,發現RequestMappingHandlerAdapter比較符合我們的目標

點進去RequestMappingHandlerAdapter後發現裡面的一個方法如下

@Override
    public void afterPropertiesSet() {
        // Do this first, it may add ResponseBody advice beans
// 這裡會添加ResponseBody advice beans
        initControllerAdviceCache();

        if (this.argumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        if (this.initBinderArgumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
            this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        if (this.returnValueHandlers == null) {
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        }
    }
// 這裡找到contollerAdvice註解的類,緩存裡面的方法
private void initControllerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }
// 找到@ControllerAdvice註解標註的類
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());

        List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
// 找到所有ModelAttribute標註的方法進行緩存,就可以使用了
            Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
            if (!attrMethods.isEmpty()) {
                this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
            }
// 找到所有initBinder註解標註的方法進行緩存,就可以使用了
            Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
            if (!binderMethods.isEmpty()) {
                this.initBinderAdviceCache.put(adviceBean, binderMethods);
            }
            if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                requestResponseBodyAdviceBeans.add(adviceBean);
            }
        }

        if (!requestResponseBodyAdviceBeans.isEmpty()) {
            this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
        }
// ......日誌處理
    }

3.2@ExceptionHandler註解的作用原理

相同的思路,@ExceptionHandler是響應時的處理,因此需要找到對應的Resolver,進入initHandlerExceptionResolvers(context)方法,

屬性填充後會進行afterPropertiesSet方法,這個方法可以用在一些特殊情況中,也就是某個對象的某個屬性需要經過外界得到,比如說查詢資料庫等方式,這時候可以用到spring的該特性,只需要實現InitializingBean。

@Override
    public void afterPropertiesSet() {
        // Do this first, it may add ResponseBodyAdvice beans
        initExceptionHandlerAdviceCache();

        if (this.argumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        if (this.returnValueHandlers == null) {
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        }
    }

private void initExceptionHandlerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }

        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
// 這裡找到ExceptionHandler註解標註的方法進行緩存,後面就可以使用了
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            if (resolver.hasExceptionMappings()) {
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
            }
            if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                this.responseBodyAdvice.add(adviceBean);
            }
        }
// ......日誌處理
    }

在啟動spring時debug發現最終也會走到這裡對@ExceptionHander註解的方法已經緩存

當Controller拋出異常時,DispatcherServlet通過ExceptionHandlerExceptionResolver來解析異常,而ExceptionHandlerExceptionResolver又通過ExceptionHandlerMethodResolver 來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標註的方法是這裡:

    @Nullable
    public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
        Method method = this.exceptionLookupCache.get(exceptionType);
        if (method == null) {
            method = getMappedMethod(exceptionType);
            this.exceptionLookupCache.put(exceptionType, method);
        }
        return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
    }

4 用具體的調用過程,驗證上面的推測

本部分通過對DispatcherServlet的調用過程跟蹤,梳理出ControllerAdvice的作用原理,以@InitBinder主節點生效過程為例。

首選是dispathServlet在初始化過程中,初始化RequestMappingHandlerAdapter過程中打斷點發現,initBinder已經緩存進來了。

然後是dispatcherServlet的調用流程圖,驗證下是initBinder註解是否生效。

DispatcherServlet 通過doService()方法開始調用,主要邏輯包括 設置 request ,通過doDispatch() 進行請求分發處理。

doDispatch() 的主要過程是通過 HandlerMapping 獲取 Handler,再找到用於執行它的 HandlerAdapter,執行 Handler 後得到 ModelAndView ,ModelAndView 是連接“業務邏輯層”與“視圖展示層”的橋梁。

4.1 DispathcerServlet的doDispatch方法

在入口處找到要執行的HandlerAdapter,調用handle方法繼續

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
// 找到執行鏈,根據請求路徑匹配到controller的方法
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
// 找到對應的HandlerAdapter,執行鏈中的handler類型為HandlerMethod的.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler. 真正進行處理的地方
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            ..........
    }

4.2 RequestmappingHanderApapter對@initBInder註解緩存方法進行處理

找到對應的handlerAdapter後進入invokeHandlerMethod()方法,在這裡通過構建WebDataBinderFactory對initBinder註解進行構建,供後續使用,具體邏輯如下。
通過getDataBinderFactory()方法從之前緩存的Map> initBinderAdviceCache中生成binderFactory

@Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
//根據initBinder註解,獲取對應的factory,主要成員是InvocableHandlerMethod,就包括之前緩存的。
            WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
            ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 創建可調用的對象,進行調用邏輯處理
            ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
            if (this.argumentResolvers != null) {
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
            }
            if (this.returnValueHandlers != null) {
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
            }
// binderFactory設置進invocableMethod,
            invocableMethod.setDataBinderFactory(binderFactory);
            invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

            ModelAndViewContainer mavContainer = new ModelAndViewContainer();
            mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
            modelFactory.initModel(webRequest, mavContainer, invocableMethod);
            mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

            AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
            asyncWebRequest.setTimeout(this.asyncRequestTimeout);

            WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
            asyncManager.setTaskExecutor(this.taskExecutor);
            asyncManager.setAsyncWebRequest(asyncWebRequest);
            asyncManager.registerCallableInterceptors(this.callableInterceptors);
            asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

            if (asyncManager.hasConcurrentResult()) {
                Object result = asyncManager.getConcurrentResult();
                mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
                asyncManager.clearConcurrentResult();
                LogFormatUtils.traceDebug(logger, traceOn -> {
                    String formatted = LogFormatUtils.formatValue(result, !traceOn);
                    return "Resume with async result [" + formatted + "]";
                });
                invocableMethod = invocableMethod.wrapConcurrentResult(result);
            }
// 繼續進行處理
            invocableMethod.invokeAndHandle(webRequest, mavContainer);
            if (asyncManager.isConcurrentHandlingStarted()) {
                return null;
            }

            return getModelAndView(mavContainer, modelFactory, webRequest);
        }
        finally {
            webRequest.requestCompleted();
        }
    }
// 生成WebDataBinderFactory的具體邏輯
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
        Class<?> handlerType = handlerMethod.getBeanType();
        Set<Method> methods = this.initBinderCache.get(handlerType);
        if (methods == null) {
            methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
            this.initBinderCache.put(handlerType, methods);
        }
        List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
        // Global methods first 獲取之前項目啟動緩存的initMethod
        this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
            if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
                Object bean = controllerAdviceBean.resolveBean();
                for (Method method : methodSet) {
                    initBinderMethods.add(createInitBinderMethod(bean, method));
                }
            }
        });
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            initBinderMethods.add(createInitBinderMethod(bean, method));
        }
        return createDataBinderFactory(initBinderMethods);
    }

經過上面的處理,發現initBinder標註的註解方法已經成功緩存進bindFactory。

4.3 繼續調用getMethodArgumentValues進行後續處理

繼續往下跟蹤,進入InvocableHandlerMethod的invokeForRequest方法,裡面有getMethodArgumentValues方法,會對請求參數進行處理。
最終使用AbstractNamedValueMethodArgumentResolver的resolveArgument()方法對請求字元串格式數據進行處理

// 請求Controller方法如下    
public ResponseData<IPage<CompanyVo>> listCompany(HttpServletRequest servletRequest, @RequestBody CompanySearch companySearch, @RequestParam LocalDate localDate){
       getLoginUser(servletRequest);
        return ResponseData.success(companyService.listCompany(companySearch));
    }

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {
// 得到方法的參數列表
        MethodParameter[] parameters = getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        }

        Object[] args = new Object[parameters.length];
// 迴圈如處理請求參數
        for (int i = 0; i < parameters.length; i++) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            args[i] = findProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (!this.resolvers.supportsParameter(parameter)) {
                throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
            }
            try {
// 真正進行參數處理的地方
                args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
            }
            catch (Exception ex) {
                // Leave stack trace for later, exception may actually be resolved and handled...
                if (logger.isDebugEnabled()) {
                    String exMsg = ex.getMessage();
                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                        logger.debug(formatArgumentError(parameter, exMsg));
                    }
                }
                throw ex;
            }
        }
        return args;
    }

// 最終會使用AbstractNamedValueMethodArgumentResolver來進行處理
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
        MethodParameter nestedParameter = parameter.nestedIfOptional();
// 得到請求參數名稱為"localdate"
        Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
        if (resolvedName == null) {
            throw new IllegalArgumentException(
                    "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        }
// 獲取請求的locadate的值,此時為字元串格式"yyyy-mm-dd"
        Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
        if (arg == null) {
            if (namedValueInfo.defaultValue != null) {
                arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
            }
            else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
            }
            arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
        }
        else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
            arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
        }
// 這裡就會使用bindFactory進行處理
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            try {
// 經過這裡進行處理,輸入的string類型就會轉為LocalDate了
                arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
            }
            catch (ConversionNotSupportedException ex) {
                throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());
            }
            catch (TypeMismatchException ex) {
                throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());
            }
            // Check for null value after conversion of incoming argument value
            if (arg == null && namedValueInfo.defaultValue == null &&
                    namedValueInfo.required && !nestedParameter.isOptional()) {
                handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
            }
        }

        handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

        return arg;
    }

最後附上上面調用過程中一些類的介紹

以上就是ControllerAdivce的全介紹。通過對源碼的學習,加深了對HTTP請求過程的理解。

參考:https://blog.csdn.net/zmm__1377445292/article/details/116158554

作者:京東物流 付鵬嘎

來源:京東雲開發者社區 自猿其說Tech


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

-Advertisement-
Play Games
更多相關文章
  • 想要搭建一個強大的後臺管理系統?本文提供了詳細的 Webman-Admin 安裝指南,幫助您快速部署和配置這個功能豐富的 Web 開發工具。瞭解如何安裝 Webman-Admin,並利用其強大的功能來管理和監控您的應用程式。立即開始搭建您的後臺管理系統,提升工作效率和用戶體驗! ...
  • 選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統採用的分配演算法是指針碰撞,而使用CMS這種基於Mark-Sweep演算法的收集器時,通常採用空閑列表。這兩種對象訪問... ...
  • 環境: centos7.9 tomcat9 jdk1.8 # 一.阿裡雲申請 [免費SSL](https://yundunnext.console.aliyun.com/?spm=5176.21213303.782131.4.304053c9wUb2BP&p=cas#/certExtend/free ...
  • python中的多線程其實並不是真正的多線程,如果想要充分地使用多核CPU資源,在python中大部分情況需要使用多進程。 python提供了非常好用的多進程包Multiprocessing,只需要定義一個函數,python會完成其它所有事情。藉助這個包,可以輕鬆完成從單進程到併發執行的轉換。 mu ...
  • Lucas定理: 主要是求$C_{n}^{m}$在模$p$情況下($mod \, p$)(一般$p$較小,而$n,m$較大的情況) 公式: $ C_{n}^{m} ≡ C_{n \, mod \, p}^{m \, mod \, p} \times C_{n/p}^{m/p} (mod \, p) ...
  • ## 測試 Spring提供了一組測試工具,可以輕鬆地測試Spring應用程式的各個組件,包括控制器、服務、存儲庫和其他組件。它具有豐富的測試註釋、實用程式類和其他功能,以幫助進行單元測試、集成測試等。 ### JPA測試 Spring JPA(Java Persistence API)是一個庫,它 ...
  • ## JAVA函數式編程 ### 函數式編程的背景和概念 維基百科:**函數式編程**,或稱**函數程式設計**、**泛函編程**(英語:Functional programming),是一種[編程範型](https://zh.wikipedia.org/wiki/編程範型),它將[電腦運算](ht ...
  • @[TOC] # Scala的基本使用 ## 一、基礎語法 ### 1.1 變數 #### 1.1.1 var和val Scala中的變數分為兩種: 可變var:可以隨時修改var聲明的變數的值 不可變val:val聲明的變數,值不能被修改,否則會報錯:error: reassignment to ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...