Controller 層代碼就該這麼寫,簡潔又優雅!

来源:https://www.cnblogs.com/javastack/archive/2022/12/19/16991912.html
-Advertisement-
Play Games

來源:https://juejin.cn/post/7123091045071454238 一個優秀的 Controller 層邏輯 說到 Controller,相信大家都不陌生,它可以很方便地對外提供數據介面。它的定位,我認為是「不可或缺的配角」。 說它不可或缺是因為無論是傳統的三層架構還是現在的 ...


來源:https://juejin.cn/post/7123091045071454238

一個優秀的 Controller 層邏輯

說到 Controller,相信大家都不陌生,它可以很方便地對外提供數據介面。它的定位,我認為是「不可或缺的配角」。

說它不可或缺是因為無論是傳統的三層架構還是現在的 COLA 架構,Controller 層依舊有一席之地,說明他的必要性。

說它是配角是因為 Controller 層的代碼一般是不負責具體的邏輯業務邏輯實現,但是它負責接收和響應請求。

從現狀看問題

Controller 主要的工作有以下幾項:

  • 接收請求並解析參數
  • 調用 Service 執行具體的業務代碼(可能包含參數校驗)
  • 捕獲業務邏輯異常做出反饋
  • 業務邏輯執行成功做出響應
//DTO
@Data
public class TestDTO {
    private Integer num;
    private String type;
}


//Service
@Service
public class TestService {

    public Double service(TestDTO testDTO) throws Exception {
        if (testDTO.getNum() <= 0) {
            throw new Exception("輸入的數字需要大於0");
        }
        if (testDTO.getType().equals("square")) {
            return Math.pow(testDTO.getNum(), 2);
        }
        if (testDTO.getType().equals("factorial")) {
            double result = 1;
            int num = testDTO.getNum();
            while (num > 1) {
                result = result * num;
                num -= 1;
            }
            return result;
        }
        throw new Exception("未識別的演算法");
    }
}


//Controller
@RestController
public class TestController {

    private TestService testService;

    @PostMapping("/test")
    public Double test(@RequestBody TestDTO testDTO) {
        try {
            Double result = this.testService.service(testDTO);
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Autowired
    public DTOid setTestService(TestService testService) {
        this.testService = testService;
    }
}

如果真的按照上面所列的工作項來開發 Controller 代碼會有幾個問題:

  • 參數校驗過多地耦合了業務代碼,違背單一職責原則
  • 可能在多個業務中都拋出同一個異常,導致代碼重覆
  • 各種異常反饋和成功響應格式不統一,介面對接不友好

改造 Controller 層邏輯

統一返回結構

統一返回值類型無論項目前後端是否分離都是非常必要的,方便對接介面的開發人員更加清晰地知道這個介面的調用是否成功(不能僅僅簡單地看返回值是否為 null 就判斷成功與否,因為有些介面的設計就是如此)。

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

使用一個狀態碼、狀態信息就能清楚地瞭解介面調用情況:

//定義返回數據結構
public interface IResult {
    Integer getCode();
    String getMessage();
}

//常用結果的枚舉
public enum ResultEnum implements IResult {
    SUCCESS(2001, "介面調用成功"),
    VALIDATE_FAILED(2002, "參數校驗失敗"),
    COMMON_FAILED(2003, "介面調用失敗"),
    FORBIDDEN(2004, "沒有許可權訪問資源");

    private Integer code;
    private String message;

    //省略get、set方法和構造方法
}

//統一返回數據結構
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }

    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
    }

    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }

    public static Result<?> failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }

    public static Result<?> failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }

    public static <T> Result<T> instance(Integer code, String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

統一返回結構後,在 Controller 中就可以使用了,但是每一個 Controller 都寫這麼一段最終封裝的邏輯,這些都是很重覆的工作,所以還要繼續想辦法進一步處理統一返回結構。

統一包裝處理

Spring 中提供了一個類 ResponseBodyAdvice ,能幫助我們實現上述需求:

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

ResponseBodyAdvice 是對 Controller 返回的內容在 HttpMessageConverter 進行類型轉換之前攔截,進行相應的處理操作後,再將結果返回給客戶端。

那這樣就可以把統一包裝的工作放到這個類裡面:

  • supports: 判斷是否要交給 beforeBodyWrite 方法執行,ture:需要;false:不需要
  • beforeBodyWrite: 對 response 進行具體的處理
// 如果引入了swagger或knife4j的文檔生成組件,這裡需要僅掃描自己項目的包,否則文檔無法正常生成
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果不需要進行封裝的,可以添加一些校驗手段,比如添加標記排除的註解
        return true;
    }


    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 提供一定的靈活度,如果body已經被包裝了,就不進行包裝
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

經過這樣改造,既能實現對 Controller 返回的數據進行統一包裝,又不需要對原有代碼進行大量的改動。

參數校驗

Java API 的規範 JSR303 定義了校驗的標準 validation-api ,其中一個比較出名的實現是 hibernate validation。

spring validation 是對其的二次封裝,常用於 SpringMVC 的參數自動校驗,參數校驗的代碼就不需要再與業務邏輯代碼進行耦合了。

①@PathVariable 和 @RequestParam 參數校驗

Get 請求的參數接收一般依賴這兩個註解,但是處於 url 有長度限制和代碼的可維護性,超過 5 個參數儘量用實體來傳參。

對 @PathVariable 和 @RequestParam 參數進行校驗需要在入參聲明約束的註解。

如果校驗失敗,會拋出 MethodArgumentNotValidException 異常。

@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {

    private TestService testService;

    @GetMapping("/{num}")
    public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
        return num * num;
    }

    @GetMapping("/getByEmail")
    public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
        TestDTO testDTO = new TestDTO();
        testDTO.setEmail(email);
        return testDTO;
    }

    @Autowired
    public void setTestService(TestService prettyTestService) {
        this.testService = prettyTestService;
    }
}
校驗原理

在 SpringMVC 中,有一個類是 RequestResponseBodyMethodProcessor,這個類有兩個作用(實際上可以從名字上得到一點啟發)

  • 用於解析 @RequestBody 標註的參數
  • 處理 @ResponseBody 標註方法的返回值

解析 @RequestBoyd 標註參數的方法是 resolveArgument。

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
      /**
     * Throws MethodArgumentNotValidException if validation fails.
     * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
     * is {@code true} and there is no body content or if there is no suitable
     * converter to read the content with.
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

      parameter = parameter.nestedIfOptional();
      //把請求數據封裝成標註的DTO對象
      Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
      String name = Conventions.getVariableNameForParameter(parameter);

      if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
          //執行數據校驗
          validateIfApplicable(binder, parameter);
          //如果校驗不通過,就拋出MethodArgumentNotValidException異常
          //如果我們不自己捕獲,那麼最終會由DefaultHandlerExceptionResolver捕獲處理
          if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
          }
        }
        if (mavContainer != null) {
          mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
      }

      return adaptArgumentIfNecessary(arg, parameter);
    }
}

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  /**
    * Validate the binding target if applicable.
    * <p>The default implementation checks for {@code @javax.validation.Valid},
    * Spring's {@link org.springframework.validation.annotation.Validated},
    * and custom annotations whose name starts with "Valid".
    * @param binder the DataBinder to be used
    * @param parameter the method parameter descriptor
    * @since 4.1.5
    * @see #isBindExceptionRequired
    */
   protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    //獲取參數上的所有註解
      Annotation[] annotations = parameter.getParameterAnnotations();
      for (Annotation ann : annotations) {
      //如果註解中包含了@Valid、@Validated或者是名字以Valid開頭的註解就進行參數校驗
         Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
         if (validationHints != null) {
        //實際校驗邏輯,最終會調用Hibernate Validator執行真正的校驗
        //所以Spring Validation是對Hibernate Validation的二次封裝
            binder.validate(validationHints);
            break;
         }
      }
   }
}
②@RequestBody 參數校驗

Post、Put 請求的參數推薦使用 @RequestBody 請求體參數。

對 @RequestBody 參數進行校驗需要在 DTO 對象中加入校驗條件後,再搭配 @Validated 即可完成自動校驗。

如果校驗失敗,會拋出 ConstraintViolationException 異常。

//DTO
@Data
public class TestDTO {
    @NotBlank
    private String userName;

    @NotBlank
    @Length(min = 6, max = 20)
    private String password;

    @NotNull
    @Email
    private String email;
}

//Controller
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {

    private TestService testService;

    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        this.testService.save(testDTO);
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}
校驗原理

聲明約束的方式,註解加到了參數上面,可以比較容易猜測到是使用了 AOP 對方法進行增強。

而實際上 Spring 也是通過 MethodValidationPostProcessor 動態註冊 AOP 切麵,然後使用 MethodValidationInterceptor 對切點方法進行織入增強。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {

    //指定了創建切麵的Bean的註解
   private Class<? extends Annotation> validatedAnnotationType = Validated.class;

    @Override
    public void afterPropertiesSet() {
        //為所有@Validated標註的Bean創建切麵
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //創建Advisor進行增強
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //創建Advice,本質就是一個方法攔截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //無需增強的方法,直接跳過
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }

        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入參校驗,最終還是委托給Hibernate Validator來校驗
             //所以Spring Validation是對Hibernate Validation的二次封裝
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //校驗不通過拋出ConstraintViolationException異常
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //Controller方法調用
        Object returnValue = invocation.proceed();
        //下麵是對返回值做校驗,流程和上面大概一樣
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}
③自定義校驗規則

有些時候 JSR303 標準中提供的校驗規則不滿足複雜的業務需求,也可以自定義校驗規則。

自定義校驗規則需要做兩件事情:

  • 自定義註解類,定義錯誤信息和一些其他需要的內容
  • 註解校驗器,定義判定規則
//自定義註解類
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    /**
     * 是否允許為空
     */
    boolean required() default true;

    /**
     * 校驗不通過返回的提示信息
     */
    String message() default "不是一個手機號碼格式";

    /**
     * Constraint要求的屬性,用於分組校驗和擴展,留空就好
     */
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

//註解校驗器
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {

    private boolean required = false;

    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 驗證手機號

    /**
     * 在驗證開始前調用註解里的方法,從而獲取到一些註解里的參數
     *
     * @param constraintAnnotation annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(Mobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }

    /**
     * 判斷參數是否合法
     *
     * @param value   object to validate
     * @param context context in which the constraint is evaluated
     */
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (this.required) {
            // 驗證
            return isMobile(value);
        }
        if (StringUtils.hasText(value)) {
            // 驗證
            return isMobile(value);
        }
        return true;
    }

    private boolean isMobile(final CharSequence str) {
        Matcher m = pattern.matcher(str);
        return m.matches();
    }
}

自動校驗參數真的是一項非常必要、非常有意義的工作。JSR303 提供了豐富的參數校驗規則,再加上複雜業務的自定義校驗規則,完全把參數校驗和業務邏輯解耦開,代碼更加簡潔,符合單一職責原則。

自定義異常與統一攔截異常

原來的代碼中可以看到有幾個問題:

  • 拋出的異常不夠具體,只是簡單地把錯誤信息放到了 Exception 中
  • 拋出異常後,Controller 不能具體地根據異常做出反饋
  • 雖然做了參數自動校驗,但是異常返回結構和正常返回結構不一致

自定義異常是為了後面統一攔截異常時,對業務中的異常有更加細顆粒度的區分,攔截時針對不同的異常作出不同的響應。

而統一攔截異常的目的一個是為了可以與前面定義下來的統一包裝返回結構能對應上,另一個是我們希望無論系統發生什麼異常,Http 的狀態碼都要是 200 ,儘可能由業務來區分系統的異常。

//自定義異常
public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) {
        super(message);
    }
}

//自定義異常
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

//統一攔截異常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {

    /**
     * 捕獲 {@code BusinessException} 異常
     */
    @ExceptionHandler({BusinessException.class})
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }

    /**
     * 捕獲 {@code ForbiddenException} 異常
     */
    @ExceptionHandler({ForbiddenException.class})
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }

    /**
     * {@code @RequestBody} 參數校驗不通過時拋出的異常處理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校驗失敗:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }

    /**
     * {@code @PathVariable} 和 {@code @RequestParam} 參數校驗不通過時拋出的異常處理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }

    /**
     * 頂級異常捕獲並統一處理,當其他異常無法處理時候選擇使用
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }

}

總結

做好了這一切改動後,可以發現 Controller 的代碼變得非常簡潔,可以很清楚地知道每一個參數、每一個 DTO 的校驗規則,可以很明確地看到每一個 Controller 方法返回的是什麼數據,也可以方便每一個異常應該如何進行反饋。

這一套操作下來後,我們能更加專註於業務邏輯的開發,代碼簡介、功能完善,何樂而不為呢?

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


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

-Advertisement-
Play Games
更多相關文章
  • 家居網購項目實現04 以下皆為部分代碼,詳見 https://github.com/liyuelian/furniture_mall.git 10.功能09-後臺管理 刪除家居 10.1需求分析/圖解 需求如下: 管理員進入到家居管理頁面 點擊刪除家居連接,彈出確認視窗,確認刪除,取消放棄 10.2 ...
  • 訓練模型過程中,經常需要追蹤一些性能指標的變化情況,以便瞭解模型的實時動態,例如:回歸任務中的MSE、分類任務中的Accuracy、生成對抗網路中的圖片、網路模型結構可視化…… 除了追蹤外,我們還希望能夠將這些指標以動態圖表的形式可視化顯示出來。 TensorFlow的附加工具Tensorboar... ...
  • 1 final基本用法 final:“這是無法改變的" final可以修飾:變數、參數、方法、類 1.1 final修飾變數 修飾變數(變數、局部變數),當變數類型為: 基本類型,一旦被賦值,該值不能被改變。 引用類型,一旦引用被初始化指向一個對象,就不能指向別的對象,但對象內容可以被修改 數據類型 ...
  • 多表連接查詢中的「多表」,可以是同一張表,自己和自己連接查詢。相當於(可以理解為) A 表自己先複製自己後再和自己連接,如此稱為「 自連接 」也可以在不同張表中連接查詢,可分為「內連接」、「交叉連接」、「外連接」。 內連接根據所使用的比較方式不同,又分為「等值連接」、「自然連接」和「不等連接」三種, ...
  • 網上的 Java 視頻教程魚龍混雜,為了防止小伙伴們踩坑,一楓結合自己的學習經驗,向大家推薦一些不錯的學習資源。 ...
  • 摘要:最近,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什麼呢?!經過仔細的分析之後,終於找到了原因。 本文分享自華為雲社區《【高併發】優化加鎖方式時竟然死鎖了!!》,作者: 冰 河。 寫在前面 最近,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什麼呢?!經過仔細的分析之後,終於找到了原 ...
  • Shell 變數(一) bash shell 編程和其他編程語言差不多,同樣包含變數(存放字元串和數值的容器,可以進行修改、比較、傳遞)。在引用 bash 變數時,可以使用一些非常特殊的運算符。bash 還擁有內建變數,這些變數可以提供有關腳本中其他變數的重要信息。下麵介紹 bash 變數和一些特殊 ...
  • Qt C++界面開發過程中,使用自定義的QGroupBox類,重寫繪圖事件paintEvent時,出現邊框被覆蓋的情況,或邊框消失的問題。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...