動態代理類註冊為Spring Bean的坑

来源:https://www.cnblogs.com/beetle-shu/archive/2022/10/16/16797409.html
-Advertisement-
Play Games

背景介紹: 最近在搭建一個公共項目,類似業務操作記錄上報的功能,就想著給業務方提供統一的sdk,在sdk中實現客戶端和服務端的交互封裝,對業務方幾乎是無感的。訪問關係如下圖: 訪問關係示意圖 這裡採用了http的方式進行交互,但是,如果每次介面調用都需要感知http的封裝,一來代碼重覆度較高,二來新 ...


背景介紹:

最近在搭建一個公共項目,類似業務操作記錄上報的功能,就想著給業務方提供統一的sdk,在sdk中實現客戶端和服務端的交互封裝,對業務方幾乎是無感的。訪問關係如下圖:

訪問關係示意圖

這裡採用了http的方式進行交互,但是,如果每次介面調用都需要感知http的封裝,一來代碼重覆度較高,二來新增或修改介面也需要同步更改客戶端代碼,就有點不太友好,維護成本較高;能否實現像調用本地方法一樣調用遠程服務(RPC)呢,當然是可以的,並且也有好多可以參考的例子。例如,feign client的實現思路,定義好服務端的介面,通過Java代理的方式創建代理類,在代理類中統一封裝了http的調用,並且將代理類作為一個bean註入到Spring容器中,使用的時候就只要獲取bean調用相應的方法即可。

寫個簡單的例子來驗證一下:

假設有個遠程服務,提供瞭如下介面:

package com.example.remoteserviceproxydemo;

/**
 * IRemoteService
 * @author beetle_shu
 */
public interface IRemoteService {

    /**
     * getGreetingName
     * @return
     */
    String getGreetingName();

    /**
     * sayHello
     * @param name
     * @return
     */
    String sayHello(String name);

}

接下來,我們自定義一個InvocationHandler 來實現遠程方法的調用

package com.example.remoteserviceproxydemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * RemoteServiceInvocationHandler
 * @author beetle_shu
 */
public class RemoteServiceInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果是遠程http服務調用,通常有以下幾步:
        // 1. 解析方法和參數:可以通過自定義註解,在方法上定義遠程服務地址,請求方式GET/POST等信息
        // 2. 採用httpclient,OkHttp,或者restTemplate進行遠程服務調用
        // 3. 解析http響應,反序列化成對應介面方法的返回對象
        // 這裡,我們就不真正調用服務了,偽代碼只是驗證下被調用的方法是不是我們自己定義的,
        // 如果是的話返回當前方法名,如果不是的話,拋出異常,程式中斷
        checkMethod(method);
        String methodName = method.getName();
        String param = "";
        if (args != null && args.length > 0) {
            param = String.valueOf(args[0]);
        }
        return methodName + ":" + param;
    }

    private void checkMethod(Method method) {
        Method[] methods = IRemoteService.class.getDeclaredMethods();
        for (Method m : methods) {
            if (m.getName().equals(method.getName())) {
                return;
            }
        }
        throw new RuntimeException("method which is not declared, " + method.getName());
    }
}

緊接著,通過java.lang.reflect.Proxy代理類創建一個代理對象,代理遠程服務的調用,同時把該對象註冊為Spring bean,加入Spring容器

package com.example.remoteserviceproxydemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Proxy;

@Configuration
public class RemoteServiceProxyDemoConfiguration {

    @Bean
    public IRemoteService getRemoteService() {
        return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
                new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
    }
}

最後,我們創建一個Controller來調用測試一下:

package com.example.remoteserviceproxydemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class DemoController {

    @Resource
    private IRemoteService iRemoteService;

    @GetMapping("/getGreetingName")
    public String getGreetingName() {
        return iRemoteService.getGreetingName();
    }

    @PostMapping("/sayHello/{name}")
    public String sayHello(@PathVariable("name") String name) {
        return iRemoteService.sayHello(name);
    }
}
###
GET http://localhost:8080/getGreetingName

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 16
Date: Thu, 06 Oct 2022 12:28:45 GMT
Connection: close

getGreetingName:

###
POST http://localhost:8080/sayHello/ketty

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Thu, 06 Oct 2022 12:30:40 GMT
Connection: close

sayHello:ketty

通過測試我們可以看到,通過代理實現了遠程介面的封裝和調用,至此,一切正常,好像沒毛病!!!可是,過了段時間就有同事找過來說依賴了我的sdk導致應用無法正常啟動了。。。

問題分析:

通過報錯的堆棧信息及debug跟蹤,最後找到問題在Spring bean的創建過程中,registerDisposableBeanIfNecessary註冊實現了Disposable Bean介面或者指定了destroy method的bean,亦或者是被指定的DestructionAwareBeanPostProcessor處理的bean,在bean銷毀的時候執行對應的方法;我們看下如下代碼片段:

/**
 * Determine whether the given bean requires destruction on shutdown.
 * <p>The default implementation checks the DisposableBean interface as well as
 * a specified destroy method and registered DestructionAwareBeanPostProcessors.
 * @param bean the bean instance to check
 * @param mbd the corresponding bean definition
 * @see org.springframework.beans.factory.DisposableBean
 * @see AbstractBeanDefinition#getDestroyMethodName()
 * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor
 */
protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
	return (bean.getClass() != NullBean.class && (DisposableBeanAdapter.hasDestroyMethod(bean, mbd) ||
      // 判斷是否有DestructionAwareBeanPostProcessors處理該bean
			(hasDestructionAwareBeanPostProcessors() && DisposableBeanAdapter.hasApplicableProcessors(
					bean, getBeanPostProcessorCache().destructionAware))));
}

繼續跟蹤到 DisposableBeanAdapter.hasApplicableProcessors

/**
 * Check whether the given bean has destruction-aware post-processors applying to it.
 * @param bean the bean instance
 * @param postProcessors the post-processor candidates
 */
public static boolean hasApplicableProcessors(Object bean, List<DestructionAwareBeanPostProcessor> postProcessors) {
	if (!CollectionUtils.isEmpty(postProcessors)) {
		for (DestructionAwareBeanPostProcessor processor : postProcessors) {
      // 每個processor根據自己的具體情況實現requiresDestruction方法,預設是返回true
			if (processor.requiresDestruction(bean)) {
				return true;
			}
		}
	}
	return false;
}

接下來,我們稍微改下代碼來重現下該問題,加入spring-boot-starter-data-jpa 以及 mapper-spring-boot-starter依賴,重新啟動應用之後,意想不到的事情發生了:

// 應用啟動報錯了,這個異常正是我們代理處理類中定義的,
// 說明應用啟動的時候,調用了iRemoteService非聲明的方法,這裡列印出來的是【hashCode】方法
Caused by: org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'iRemoteService' defined in class path resource 
[com/example/remoteserviceproxydemo/RemoteServiceProxyDemoConfiguration.class]: 
Unexpected exception during bean creation; nested exception is java.lang.RuntimeException: 
method which is not declared, hashCode

通過以上代碼分析,我們找到了調用的地方,PersistenceAnnotationBeanPostProcessor.requiresDestruction` 方法,這裡最終會執行註冊bean的hashCode方法,由於是代理類,所以會執行InvocationHandler的invoke方法;而hashCode方法並不是我們IRemoteService介面類中聲明的方法,所以會在checkMethod中拋出異常

@Override
public boolean requiresDestruction(Object bean) {
  // 這裡extendedEntityManagersToClose是ConcurrentHashMap
	return this.extendedEntityManagersToClose.containsKey(bean);
}

// ConcurrentHashMap的containsKey方法
/**
 * Tests if the specified object is a key in this table.
 *
 * @param  key possible key
 * @return {@code true} if and only if the specified object
 *         is a key in this table, as determined by the
 *         {@code equals} method; {@code false} otherwise
 * @throws NullPointerException if the specified key is null
 */
public boolean containsKey(Object key) {
    return get(key) != null;
}

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>More formally, if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that {@code key.equals(k)},
 * then this method returns {@code v}; otherwise it returns
 * {@code null}.  (There can be at most one such mapping.)
 *
 * @throws NullPointerException if the specified key is null
 */
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 這裡可以看到,調用了hashCode方法,由於該bean是代理類,
    // 所以會執行RemoteServiceInvocationHandler的invoke方法,
    // 從而拋出自定義異常throw new RuntimeException("method which is not declared, " + method.getName());
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

解決方法:

  1. 不用代理類,寫個具體實現類

    這種方法跟我們初衷有點相背離,以後介面新增修改也都要改sdk中的實現類,具體實現如下:

    package com.example.remoteserviceproxydemo;
    
    import java.lang.reflect.Proxy;
    
    // 定義具體的實現類
    public class RemoteServiceImpl implements IRemoteService {
    
        private IRemoteService iRemoteService;
    
        public RemoteServiceImpl() {
            this.iRemoteService = (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
                    new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
        }
    
        @Override
        public String getGreetingName() {
            return iRemoteService.getGreetingName();
        }
    
        @Override
        public String sayHello(String name) {
            return iRemoteService.sayHello(name);
        }
    }
    
    package com.example.remoteserviceproxydemo;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.lang.reflect.Proxy;
    
    @Configuration
    public class RemoteServiceProxyDemoConfiguration {
    
        @Bean("iRemoteService")
        public IRemoteService getRemoteService() {
    // 註冊的bean也改為具體實現類,這樣就可以繞過代理類沒有【hashCode】方法的問題了
            return new RemoteServiceImpl();
    //        return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
    //                new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
        }
    
    }
    
  2. 用代理類,在invoke方法中對【hashCode】方法調用做特殊處理

    這種方法也是參考feign的實現,改起來也比較簡單,invoke方法進來先判斷是hashCode/equals/toString方法,就執行重寫的hashCode/equals/toString方法,改寫RemoteServiceInvocationHandler如下 :

    package com.example.remoteserviceproxydemo;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * RemoteServiceInvocationHandler
     * @author beetle_shu
     */
    public class RemoteServiceInvocationHandler implements InvocationHandler {
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 新增對hashCode/equals/toString方法的處理
            if ("equals".equals(method.getName())) {
                try {
                    Object otherHandler =
                            args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    // 可以根據實際情況重寫【equals】方法
                    return this.equals(otherHandler);
                } catch (IllegalArgumentException e) {
                    return false;
                }
            } else if ("hashCode".equals(method.getName())) {
                // 可以根據實際情況重寫【hashCode】方法
                return this.hashCode();
            } else if ("toString".equals(method.getName())) {
                // 可以根據實際情況重寫【toString】方法
                return this.toString();
            }
            // 如果是遠程http服務調用,通常有以下幾步:
            // 1. 解析方法和參數:可以通過自定義註解,在方法上定義遠程服務地址,請求方式GET/POST等信息
            // 2. 採用httpclient,OkHttp,或者restTemplate進行遠程服務調用
            // 3. 解析http響應,反序列化成對應介面方法的返回對象
            // 這裡,我們就不真正調用服務了,偽代碼僅返回當前方法名
            checkMethod(method);
            String methodName = method.getName();
            String param = "";
            if (args != null && args.length > 0) {
                param = String.valueOf(args[0]);
            }
            return methodName + ":" + param;
        }
    
        private void checkMethod(Method method) {
            Method[] methods = IRemoteService.class.getDeclaredMethods();
            for (Method m : methods) {
                if (m.getName().equals(method.getName())) {
                    return;
                }
            }
            throw new RuntimeException("method which is not declared, " + method.getName());
        }
    }
    
  3. 用FactoryBean的getObject返回代理類,並且自定義BeanDefinitionRegistrar註冊bean

    這種方法也是我比較推薦的,很好的利用了Spring的擴展,進行動態bean的註冊;當然,結合第2種方法一起實現,應該會完美:

    package com.example.remoteserviceproxydemo;
    
    import org.springframework.beans.factory.FactoryBean;
    
    import java.lang.reflect.Proxy;
    
    /**
     * 定義RemoteServiceFactoryBean
     * @author beetle_shu
     */
    public class RemoteServiceFactoryBean implements FactoryBean<IRemoteService> {
    
        @Override
        public IRemoteService getObject() throws Exception {
            return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
                    new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
        }
    
        @Override
        public Class<?> getObjectType() {
            return IRemoteService.class;
        }
    
        @Override
        public boolean isSingleton() {
            return true;
        }
    }
    

    自定義BeanDefinitionRegistryPostProcessor 並且通過FactoryBean註冊iRemoteService

    package com.example.remoteserviceproxydemo;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
    
    /**
     * RemoteServiceBeanDefinitionRegistryPostProcessor
     * @author beetle_shu
     */
    public class RemoteServiceBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
            BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(RemoteServiceFactoryBean.class);
            registry.registerBeanDefinition("iRemoteService", definitionBuilder.getBeanDefinition());
        }
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    
        }
    }
    

    修改下配置類,通過@Import載入RemoteServiceBeanDefinitionRegistryPostProcessor

    package com.example.remoteserviceproxydemo;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    
    import java.lang.reflect.Proxy;
    
    @Configuration
    @Import(RemoteServiceBeanDefinitionRegistryPostProcessor.class)
    public class RemoteServiceProxyDemoConfiguration {
    
    //    @Bean("iRemoteService")
    //    public IRemoteService getRemoteService() {
    ////        return new RemoteServiceImpl();
    //        return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
    //                new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
    //    }
    
    }
    
  4. 重寫PersistenceAnnotationBeanPostProcessor

    個人不太建議用這種方式,除非對Spring框架有比較透徹的理解以及對源代碼有比較高的把控度,具體實現可以參考該大神的文章:https://www.huluohu.com/posts/202102252023/

總結:

雖說是個小問題也比較細節,但是,整個過程梳理下來還是涉及到很多的知識點:Spring boot啟動過程;Spring bean的生命周期;Spring boot擴展BeanPostProcessor; FactoryBean的用法;動態註冊Spring bean的幾種方法;Java反射及代理等等。通過這些知識的梳理,重新回顧的同時也學到了一些新的知識,希望以後能多抓住這種排查問題和分析問題的機會,多多總結,少踩坑。

參考:

  1. 如何記憶 Spring Bean 的生命周期 https://juejin.cn/post/6844904065457979405
  2. 三萬字盤點Spring/Boot的那些擴展點 https://mdnice.com/writing/97dd3ca064304bc9b8d3231dbba2f3b8
  3. jpa調用遠程代理類的hashcode方法導致無法初始化的問題 https://www.huluohu.com/posts/202102252023/
  4. 動態註冊bean,Spring官方套路:使用BeanDefinitionRegistryPostProcessor https://zhuanlan.zhihu.com/p/30590254
  5. 使用BeanDefinitionRegistryPostProcessor動態註入BeanDefinition https://www.jianshu.com/p/b4bec64ada70

代碼示例:


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

-Advertisement-
Play Games
更多相關文章
  • 隨著系統運行時間的推移,資料庫日誌文件會變得越來越大,這時我們需要對日誌文件進行備份或清理。 解決方案1 - 直接刪除本地ldf日誌文件:(比較靠譜方案!) 1. 在SQL管理器分離資料庫。 2. 對資料庫日誌文件進行壓縮備份(rar, zip) 3. 直接刪除ldf文件。 4. 再附加資料庫。若出 ...
  • MongoDB 是一個強大的分散式存儲引擎,天然支持高可用、分散式和靈活設計。MongoDB 的一個很重要的設計理念是:服務端只關註底層核心能力的輸出,至於怎麼用,就儘可能的將工作交個客戶端去決策。這也就是 MongoDB 靈活性的保證,但是靈活性帶來的代價就是使用成本的提升。與 MySql 相比,... ...
  • 我們在安卓開發學習中會遇到需要返回數據的情況,這裡我們使用了幾個方法 1、startActivityForResult通過這個方法我們可以啟動另外一個活動 2、onBasePressed使用這個方法我們可以 點擊返回鍵返回數據到上一個活動 3、onActivityResult我們在需要接收返回數據的 ...
  • 語法規範 JavaScript嚴格區分大小寫,對空格、換行、縮進不敏感,建議語句結束加‘;’ JavaScript 會忽略多個空格。您可以向腳本添加空格,以增強可讀性。 JavaScript 程式員傾向於使用以小寫字母開頭的駝峰大小寫 firstName, lastName, masterCard, ...
  • Openlayers介紹 ​ Openlayers是一個基於Javacript開發,免費、開源的前端地圖開發庫,使用它,可以很容易的開發出WebGIS系統。目前Openlayers支持地圖瓦片、矢量數據等眾多地圖數據格式,支持比較完整的地圖交互操作。目前OpenLayers已經成為一個擁有眾多開發者 ...
  • 一篇文章帶你掌握主流辦公框架——SpringBoot 在之前的文章中我們已經學習了SSM的全部內容以及相關整合 SSM是Spring的產品,主要用來簡化開發,但我們現在所介紹的這款框架——SpringBoot,卻是用來簡化Spring開發的框架 SpringBoot是由Pivowtal團隊提供的全新 ...
  • 1.前言 首先回顧下代理模式(Proxy Pattern)的定義:代理模式指為其他對象提供一種代理,以控制這個對象的訪問,屬於結構型設計模式。其適用於在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端於目標對象之間起到中介的作用。 代理模式主要分為靜態代理和動態代理兩種方 ...
  • 挺早以前就刷了裡面一些題,結果不知道為啥登錄賬號刷題記錄又沒了,強迫症又讓我不想從中間開始刷。既然如此,那就從頭開始刷吧。QWQ Step one 第一題,沒啥好說的。 module top_module( output one ); // Insert your code here assign ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...