Spring自定義參數解析器設計

来源:https://www.cnblogs.com/Jcloud/archive/2023/04/14/17317545.html
-Advertisement-
Play Games

@RequstBody、@RequstParam 這些註解是不是很熟悉?我們在開發Controller介面時經常會用到此類參數註解,那這些註解的作用是什麼?我們真的瞭解嗎? ...


作者:京東零售 王鵬超

1.什麼是參數解析器

@RequstBody、@RequstParam 這些註解是不是很熟悉?

我們在開發Controller介面時經常會用到此類參數註解,那這些註解的作用是什麼?我們真的瞭解嗎?

簡單來說,這些註解就是幫我們將前端傳遞的參數直接解析成直接可以在代碼邏輯中使用的javaBean,例如@RequstBody接收json參數,轉換成java對象,如下所示:

前臺傳參 參數格式
application/json

正常代碼書寫如下:

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
    //***
    return userInfo.getName();
}



但如果是服務接收參數的方式改變了,如下代碼,參數就不能成功接收了,這個是為什麼呢?

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
    //***
    return userName;
}



如果上面的代碼稍微改動一下註解的使用並且前臺更改一下傳參格式,就可以正常解析了。

前臺傳參 參數格式
http://***?userName=Alex&userId=1
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
    //***
    return userName;
}



這些這裡就不得不引出這些註解背後都對應的內容—Spring提供的參數解析器,這些參數解析器幫助我們解析前臺傳遞過來的參數,綁定到我們定義的Controller入參上,不通類型格式的傳遞參數,需要不同的參數解析器,有時候一些特殊的參數格式,甚至需要我們自定義一個參數解析器。

不論是在SpringBoot還是在Spring MVC中,一個HTTP請求會被DispatcherServlet類接收(本質是一個Servlet,繼承自HttpServlet)。Spring負責從HttpServlet中獲取並解析請求,將請求uri匹配到Controller類方法,並解析參數並執行方法,最後處理返回值並渲染視圖。

參數解析器的作用就是將http請求提交的參數轉化為我們controller處理單元的入參。原始的Servlet獲取參數的方式如下,需要手動從HttpServletRequest中獲取所需信息。

@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        /**獲取參數開始*/
        String resourceId = req.getParameter("resourceId");
        String resourceType = req.getHeader("resourceType");
        /**獲取參數結束*/
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("resourceId " + resourceId + " resourceType " + resourceType);
    }
}



Spring為了幫助開發者解放生產力,提供了一些特定格式(header中content-type對應的類型)入參的參數解析器,我們在介面參數上只要加上特定的註解(當然不加註解也有預設解析器),就可以直接獲取到想要的參數,不需要我們自己去HttpServletRequest中手動獲取原始入參,如下所示:

@RestController
public class resourceController {

  @RequestMapping("/resource")
  public String getResource(@RequestParam("resourceId") String resourceId,
            @RequestParam("resourceType") String resourceType,
            @RequestHeader("token") String token) {
    return "resourceId" + resourceId + " token " + token;
  }
}



常用的註解類參數解析器使用方式以及與註解的對應關係對應關係如下:

註解命名 放置位置 用途
@PathVariable 放置在參數前 允許request的參數在url路徑中
@RequestParam 放置在參數前 允許request的參數直接連接在url地址後面,也是Spring預設的參數解析器
@RequestHeader 放置在參數前 從請求header中獲取參數
@RequestBody 放置在參數前 允許request的參數在參數體中,而不是直接連接在地址後面
註解命名 對應的解析器 content-type
@PathVariable PathVariableMethodArgumentResolver
@RequestParam RequestParamMethodArgumentResolver 無(get請求)和multipart/form-data
@RequestBody RequestResponseBodyMethodProcessor application/json
@RequestPart RequestPartMethodArgumentResolver multipart/form-data

2.參數解析器原理

要瞭解參數解析器,首先要瞭解一下最原始的Spring MVC的執行過程。客戶端用戶發起一個Http請求後,請求會被提交到前端控制器(Dispatcher Servlet),由前端控制器請求處理器映射器(步驟1),處理器映射器會返回一個執行鏈(Handler Execution 步驟2),我們通常定義的攔截器就是在這個階段執行的,之後前端控制器會將映射器返回的執行鏈中的Handler信息發送給適配器(Handler Adapter 步驟3),適配器會根據Handler找到並執行相應的Handler邏輯,也就是我們所定義的Controller控制單元(步驟4),Handler執行完畢會返回一個ModelAndView對象,後續再經過視圖解析器解析和視圖渲染就可以返回給客戶端請求響應信息了。

在容器初始化的時候,RequestMappingHandlerMapping 映射器會將 @RequestMapping 註解註釋的方法存儲到緩存,其中key是 RequestMappingInfo,value是HandlerMethod。HandlerMethod 是如何進行方法的參數解析和綁定,就要瞭解請求參數適配器**RequestMappingHandlerAdapter,**該適配器對應接下來的參數解析及綁定過程。源碼路徑如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

RequestMappingHandlerAdapter大致的解析和綁定流程如下圖所示,

RequestMappingHandlerAdapter實現了介面InitializingBean,在Spring容器初始化Bean後,調用方法afterPropertiesSet( ),將預設參數解析器綁定HandlerMethodArgumentResolverComposite 適配器的參數 argumentResolvers上,其中HandlerMethodArgumentResolverComposite是介面HandlerMethodArgumentResolver的實現類。源碼路徑如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet

@Override
public void afterPropertiesSet() {
   // Do this first, it may add 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);
   }
}



通過getDefaultArgumentResolvers( )方法,可以看到Spring為我們提供了哪些預設的參數解析器,這些解析器都是HandlerMethodArgumentResolver介面的實現類。

針對不同的參數類型,Spring提供了一些基礎的參數解析器,其中有基於註解的解析器,也有基於特定類型的解析器,當然也有兜底預設的解析器,如果已有的解析器不能滿足解析要求,Spring也提供了支持用戶自定義解析器的擴展點,源碼如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
   List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

   // Annotation-based argument resolution 基於註解
   /** @RequestPart 文件註入 */
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
   /** @RequestParam 名稱解析參數 */
   resolvers.add(new RequestParamMapMethodArgumentResolver());
   /** @PathVariable url路徑參數 */
   resolvers.add(new PathVariableMethodArgumentResolver());
   /** @PathVariable url路徑參數,返回一個map */
   resolvers.add(new PathVariableMapMethodArgumentResolver());
   /** @MatrixVariable url矩陣變數參數 */
   resolvers.add(new MatrixVariableMethodArgumentResolver());
   /** @MatrixVariable url矩陣變數參數 返回一個map*/
   resolvers.add(new Matrix VariableMapMethodArgumentResolver());
   /** 兜底處理@ModelAttribute註解和無註解 */
   resolvers.add(new ServletModelAttributeMethodProcessor(false));
   /** @RequestBody body體解析參數 */
   resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestPart 使用類似RequestParam */
   resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestHeader 解析請求header */
   resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
   /** @RequestHeader 解析請求header,返回map */
   resolvers.add(new RequestHeaderMapMethodArgumentResolver());
   /** Cookie中取值註入 */
   resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
   /** @Value */
   resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
   /** @SessionAttribute */
   resolvers.add(new SessionAttributeMethodArgumentResolver());
   /** @RequestAttribute */
   resolvers.add(new RequestAttributeMethodArgumentResolver());

   // Type-based argument resolution 基於類型
   /** Servlet api 對象 HttpServletRequest 對象綁定值 */
   resolvers.add(new ServletRequestMethodArgumentResolver());
   /** Servlet api 對象 HttpServletResponse 對象綁定值 */
   resolvers.add(new ServletResponseMethodArgumentResolver());
   /** http請求中 HttpEntity RequestEntity數據綁定 */
   resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** 請求重定向 */
   resolvers.add(new RedirectAttributesMethodArgumentResolver());
   /** 返回Model對象 */
   resolvers.add(new ModelMethodProcessor());
   /** 處理入參,返回一個map */
   resolvers.add(new MapMethodProcessor());
   /** 處理錯誤方法參數,返回最後一個對象 */
   resolvers.add(new ErrorsMethodArgumentResolver());
   /** SessionStatus */
   resolvers.add(new SessionStatusMethodArgumentResolver());
   /**  */
   resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

   // Custom arguments 用戶自定義
   if (getCustomArgumentResolvers() != null) {
      resolvers.addAll(getCustomArgumentResolvers());
   }

   // Catch-all 兜底預設
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
   resolvers.add(new ServletModelAttributeMethodProcessor(true));

   return resolvers;
}



HandlerMethodArgumentResolver介面中只定義了兩個方法,分別是解析器適用範圍確定方法supportsParameter( )和參數解析方法resolveArgument(),不同用途的參數解析器的使用差異就體現在這兩個方法上,這裡就不具體展開參數的解析和綁定過程。

3.自定義參數解析器的設計

Spring的設計很好踐行了開閉原則,不僅在封裝整合了很多非常強大的能力,也為用戶留好了自定義拓展的能力,參數解析器也是這樣,Spring提供的參數解析器基本能滿足常用的參數解析能力,但很多系統的參數傳遞並不規範,比如京東color網關傳業務參數都是封裝在body中,需要先從body中取出業務參數,然後再針對性解析,這時候Spring提供的解析器就幫不了我們了,需要我們擴展自定義適配參數解析器了。

Spring提供兩種自定義參數解析器的方式,一種是實現適配器介面HandlerMethodArgumentResolver,另一種是繼承已有的參數解析器(HandlerMethodArgumentResolver介面的現有實現類)例如AbstractNamedValueMethodArgumentResolver進行增強優化。如果是深度定製化的自定義參數解析器,建議實現自己實現介面進行開發,以實現介面適配器介面自定義開發解析器為例,介紹如何自定義一個參數解析器。

通過查看源碼發現,參數解析適配器介面留給我擴展的方法有兩個,分別是supportsParameter( )和resolveArgument( ),第一個方法是自定義參數解析器適用的場景,也就是如何命中參數解析器,第二個是具體解析參數的實現。

public interface HandlerMethodArgumentResolver {

   /**
    * 識別到哪些參數特征,才使用當前自定義解析器
    */
   boolean supportsParameter(MethodParameter parameter);

   /**
    * 具體參數解析方法
    */
   Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}




現在開始具體實現一個基於註解的自定義參數解析器,這個是代碼實際使用過程中用到的參數解析器,獲取color網關的body業務參數,然後解析後給Controller方法直接使用。

public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
    private static final String DEFAULT_VALUE = "body";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        /** 只有指定註解註釋的參數才會走當前自定義參數解析器 */
        return parameter.hasParameterAnnotation(RequestJsonParam.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        /** 獲取參數註解 */
        RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
        
        /** 獲取參數名 */
        String name = attribute.value();
        /** 獲取指定名字參數的值 */
        String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
        /** 獲取註解設定參數類型 */
        Class<?> targetParamType = attribute.recordClass();
        /** 獲取實際參數類型 */
        Class<?> webParamType = parameter.getParameterType()
        /** 以自定義參數類型為準 */
        Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
        if (ObjectUtils.equals(paramType, String.class) 
            || ObjectUtils.equals(paramType, Integer.class)
            || ObjectUtils.equals(paramType, Long.class) 
            || ObjectUtils.equals(paramType, Boolean.class)) {
                JSONObject object = JSON.parseObject(value);
                log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
                if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
                    //入參:Integer  目標類型:Long
                    result = paramType.cast(((Integer) object.get(paramName)).longValue());
                }else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
                    //入參:Integer  目標類型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
                    //入參:Long  目標類型:Integer(精度丟失)
                    result = paramType.cast(((Long) object.get(paramName)).intValue());
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
                    //入參:Long  目標類型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
                    //入參:String  目標類型:Long
                    result = Long.valueOf((String) object.get(paramName));
                } else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
                    //入參:String  目標類型:Integer
                    result = Integer.valueOf((String) object.get(paramName));
                } else {
                    result = paramType.cast(object.get(paramName));
                }
        }else if (paramType.isArray()) {
            /** 入參是數組 */
            result = JsonHelper.fromJson(value, paramType);
            if (result != null) {
                Object[] targets = (Object[]) result;
                for (int i = 0; i < targets.length; i++) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
                   validateIfApplicable(binder, parameter, annotations);
                }
             }
       } else if (Collection.class.isAssignableFrom(paramType)) {
            /** 這裡要特別註意!!!,集合參數由於範型獲取不到集合元素類型,所以指定類型就非常關鍵了 */
            Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
            result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
            if (result != null) {
               Collection<Object> targets = (Collection<Object>) result;
               int index = 0;
               for (Object targetObj : targets) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
                   validateIfApplicable(binder, parameter, annotations);
               }
            }
        } else{
              result = JSON.parseObject(value, paramType);
        }
    
        if (result != null) {
            /** 參數綁定 */
            WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
            result = binder.convertIfNecessary(result, paramType, parameter);
            validateIfApplicable(binder, parameter, annotations);
            mavContainer.addAttribute(name, result);
        }
    }



自定義參數解析器註解的定義如下,這裡定義了一個比較特殊的屬性recordClass,後續會講到是解決什麼問題。

/**
 * 請求json參數處理註解
 * @author wangpengchao01
 * @date 2022-11-07 14:18
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
    /**
     * 綁定的請求參數名
     */
    String value() default "body";

    /**
     * 參數是否必須
     */
    boolean required() default false;

    /**
     * 預設值
     */
    String defaultValue() default ValueConstants.DEFAULT_NONE;

    /**
     * 集合json反序列化後記錄的類型
     */
    Class recordClass() default null;
}



通過配置類將自定義解析器註冊到Spring容器中

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
        return new ActMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(actMethodArgumentResolverConfigurer());
    }
}



到此,一個完整的基於註解的自定義參數解析器就完成了。

4.總結

瞭解Spring的參數解析器原理有助於正確使用Spring的參數解析器,也讓我們可以設計適用於自身系統的參數解析器,對於一些通用參數類型的解析減少重覆代碼的書寫,但是這裡有個前提是我們項目中複雜類型的入參要統一前端傳遞參數的格式也要統一,不然設計自定義參數解析器就是個災難,需要做各種複雜的相容工作。參數解析器的設計儘量要放在項目開發開始階段,歷史複雜的系統如果介面開發沒有統一規範也不建議自定義參數解析器設計。

該文章僅作為Spring參數解析器的介紹性解讀,希望對大家有所幫助,歡迎有這類需求或者興趣的同學溝通交流,批評指正,一起進步!


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

-Advertisement-
Play Games
更多相關文章
  • Java + Jpcap實現監控 IP包流量 說明:本設計是電腦網路課程的課設,因為代碼是提前實現的,本博客於後期補上,又因為代碼沒寫註釋自己也看不懂了,所以,僅供參考,就當提供一種實現方式。 文中提供的《Jpcap中文API文檔》來源於網路,本文僅用於學習交流,如有侵權,可聯繫我進行刪除。 效果 ...
  • 是什麼 迴圈隊列, FIFO先進先出 怎麼用 初始化 //C11 deque<int> deq{1,2,3,4,5}; //拷貝構造,可以拷貝deque queue<int> que(deq); //100個5 queue<int> que2(100,5); //運算符重載 que2 = que; ...
  • 集合的理解和好處 數組一旦定義,長度即固定,不能修改。要添加新元素需要新建數組,然後迴圈拷貝,非常麻煩 集合可以動態保存任意多個對象,使用比較方便 提供餓了一系列方便的操作對象的方法:add、remove、set、get等 使用集合添加、刪除新元素的示意代碼,簡潔明瞭 集合主要是兩組(單列集合,雙列 ...
  • 本文已經收錄到Github倉庫,該倉庫包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~ Github地址 如果訪問不了Github,可以訪 ...
  • 繼承(Inheritance) Odoo的一個強大方面是它的模塊化。模塊專用於業務需求,但模塊也可以相互交互。這對於擴展現有模塊的功能非常有用。例如,在我們的房地產場景中,我們希望在常規用戶視圖中直接顯示銷售人員的財產列表。 在介紹特定的Odoo模塊繼承之前,讓我們看看如何更改標準CRUD(創建、檢 ...
  • 項目介紹與環境搭建 1.項目學習前置知識 Java基礎知識 javaweb MySQL SpringBoot SSM(Spring,SpringMVC,MyBatis) Maven 2.學習收穫 瞭解企業項目開發的完整流程,增長開發經驗 瞭解需求分析的過程,提高分析和設計能力 對所學的技術進行靈活應 ...
  • 簡述 GDB, the GNU Project debugger, allows you to see what is going on 'inside' another program while it executes -- or what another program was doing a ...
  • token驗證 什麼是token?我相信很多開發者都或多或少聽過基於 token 的用戶鑒權和基於 session 的用戶鑒權,而今天說的 token 驗證就是第一種了。token 的意思是“令牌”,是用戶第一次登錄伺服器返回的,它能讓用戶不需要提交賬戶和密碼就能進行伺服器驗證身份,它是被放在請求頭 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...