1. 程式人生 > 其它 >SpringBoot實戰 - 秒殺系統構建(2)

SpringBoot實戰 - 秒殺系統構建(2)

技術標籤:專案實戰springjavaajaxweb

SpringBoot實戰 - 秒殺系統構建(2)

至此,整個秒殺專案已構建完成,此係列的兩篇博文挑選出在學習構建這個專案過成中學習到的一些開發規範和新知識進行分享,感謝閱讀
參考資料:https://www.imooc.com/learn/1079
原始碼GitHub連結:https://github.com/KajuneLeon/MiaoshaProject


個人部落格:http://www.jiajunliang.com

0. 回顧

在上一篇博文中,主要介紹了一些web開發規範,包括有:Model、ViewObject、DataObject和CommonReturnType

  • CommonReturnType:主要用於前後端分離,具有data和status欄位,status欄位用於標識請求和後端服務處理是否成功,data儲存資料或錯誤資訊,然後以json方式返回給前端
  • ViewObject:接入層模型,由Controller管理使用,與前端對接,隱藏內部實現,供展示
  • Model:業務層模型,由Service管理使用,作為領域模型,是業務核心,擁有生命週期,可由多個不同DataObject組合關聯而成,是貧血模型(只包含資料,不包含業務邏輯的類)並以服務(Service)輸出能力
  • DataObject:資料層模型,由Dao管理使用,用於資料庫對映,並以ORM方式操作資料庫的能力模型

1. 後端校驗

在web開發的過程中,為了確保請求資料的有效性,一般都需要進行資料校驗。資料校驗分為前端校驗和後端校驗,兩者同等重要。缺少了前端校驗,使用者無法獲得良好的使用體驗,而缺少了後端校驗,則使用者可通過禁用js隨意提交請求資料,無法保障安全性。在這裡主要介紹後端校驗
開發中,為了簡化開發流程會使用一些工具進行自動化校驗,如此處使用的org.hibernate.validator,為了使用該工具,首先需要在pom.xml中引入依賴

<dependency>
    <
groupId
>
org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.13.Final</version> </dependency>

引入依賴後可以在專案中新建一個validator包,包中有ValidationResult類和ValidatorImpl類,結構如下:

  • ValidationResult:該類用於在應用程式與validator邏輯校驗之間提供對接,儲存校驗結果和錯誤資訊
  • ValidatorImpl:該類實現了Spring的InitializingBean介面,在該類的生命週期的初始化階段,使用hibernate validator的工廠方法例項化校驗器,並且提供了通用的自動化校驗方法
@Data
public class ValidationResult {

    //校驗結果是否有錯
    private boolean hasErrors = false;
    //存放錯誤資訊的map
    private Map<String, String> errorMsgMap = new HashMap<>();

    public boolean isError(){
        return hasErrors;
    }

    //實現通用的通過格式化字串資訊獲取錯誤結果的msg方法
    public String getErrMsg(){
        return StringUtils.join(errorMsgMap.values().toArray(), ",");
    }
}
@Component
public class ValidatorImpl implements InitializingBean {

    private Validator validator;

    //實現校驗方法並返回校驗結果
    public ValidationResult validate(Object bean){
        ValidationResult result = new ValidationResult();
        //當bean裡的屬性有違背了對應Validation註解的規則時,constraintViolationSet就會有該屬性值
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean);
        if(constraintViolationSet.size() > 0){
            //有錯誤
            result.setHasErrors(true);
            constraintViolationSet.forEach(constraintViolation->{
                String errMsg = constraintViolation.getMessage();
                String propertyName = constraintViolation.getPropertyPath().toString();
                result.getErrorMsgMap().put(propertyName, errMsg);
            });
        }
        return result;
    }

    //bean生命週期——初始化
    @Override
    public void afterPropertiesSet() throws Exception {
        //將hibernate validator通過工廠的初始化方式使校驗器例項化
        this.validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
}

2. 跨域請求

在這裡,整個秒殺系統是基於前後端分離的實現,即前端html頁面通過動態ajax請求獲取後端伺服器服務,然後伺服器以json的方式響應,前端獲取後再解析到頁面上,這種開發方式比較簡便,但也很容易會引入跨域請求問題

2.1. 產生跨域請求的原因

瀏覽器中使用ajax請求(XMLHttpRequest),並且訪問的域名不同就會產生跨域請求問題,比如:在瀏覽器中訪問的HTML URL是file:///D:/JavaProject/MiaoshaProject/html/listitem.html,但內部js傳送的ajax請求URL是http://localhost:8090/order/createorder

2.2. 解決跨域請求

2.2.1. JSONP方式

jsonp替換了ajax請求,從而避免產生跨域請求問題

$.ajax({    
    type : "GET",
    url : "http://api.map.baidu.com/geocoder/v2/",
    data:"address=上海",
    dataType:"jsonp",
    jsonp:"callback",
    jsonpCallback:"showLocation",
    success : function(data){
        alert("成功");
    },
    error : function(data){
        alert("失敗");
    }
});

上述程式碼看似使用了ajax請求,但jsonp和jsonpCallback選項將上述程式碼翻譯為HTML頁面上的DOM,類似於

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type='text/javascript'>
            // 後端返回直接執行的方法,相當於執行這個方法,由於後端把返回的資料放在方法的引數裡,所以這裡能拿到res
            window.showLocation = function (res) {
                console.log(res)
                //執行ajax回撥
            }
        </script>
        <script src='http://api.map.baidu.com/geocoder/v2/?address=上海&callback=showLocation' type='text/javascript'></script>
    </body>
</html>

此時,HTML的<script src=‘...’>標籤訪問其中URL指定的伺服器,且script、img這類標籤不受XMLHttpRequest的限制,可以隨意訪問,不會造成跨域請求問題;然後根據雙方約定好的callback引數,返回一個被包裝後的json,即showLocation({...});接著瀏覽器直接執行了這個showLocation({...})即上述程式碼中的window.showLocation = function (res),其中的res引數就是被包裝的json

2.2.2. CORS

CORS是一個W3C標準,全稱是“跨域資源共享”,用於解除瀏覽器的限制,

  • Access-Control-Allow-Origin: *表明允許所有的origin(瀏覽器的html頁面路徑)訪問,而並非是同源的origin
  • Access-Control-Request-Method: *表明允許所有的http request頭訪問,因為瀏覽器在觸發如下幾個場景會在傳送真正的資料前傳送options這樣的預檢請求檢測,一旦預檢通過後才會傳送真正的get或post資料請求,這個時候我們按照cors的設定就需要允許對應的method訪問,觸發的幾種情況包括:1-請求的方法不是GET/HEAD/POST,2-POST請求的Content-Type並非application/x-www-form-urlencoded、multipart/form-data或text/plain,3-請求設定了自定義的header欄位等
  • Access-Control-Allow-Headers: *設定所有header均可以被允許,這個配置聯通上述的request method options檢測一起使用,可以在需要自定義header的場景下使用
  • Access-Control-Allow-Credentials:true這個引數只有當需要跨域使用cookie傳遞時才需要設定為true,並且需要前端ajax配置使用xhrField:{withCredential:true}時才能傳遞cookie,另外safari和最新版本的chrome瀏覽器還需要在設定內放開對應限制,當這個引數被設定成true時候Access-Control-Allow-Origin就不能設定為*,否則就變成任何origin域都能允許傳遞cookie了,可將其調整為前端origin欄位傳什麼我就用什麼

SpringBoot實現CROS

SpringBoot中可在Controller類上添加註解@CrossOrigin實現,如:@CrossOrigin(allowCredentials="true", allowedHeaders="*", originPatterns="*")

  • 注意事項:如果缺少了originPatterns="*"會報錯
  • 原因:由下Debug資訊可見,@CrossOrigin中,origins(Access-Control-Allow-Origin)預設為*,而當allowCredentials為true時,Access-Control-Allow-Origin就不能設定為*,因此為了允許跨域傳遞cookie(即allowCredentials),需要顯式提供允許跨域傳遞的origin(源),即引數originPatterns="*";這裡為了簡單設為*進行通用匹配,但一般為了安全需求會提供更具體的origin值

Debug:When allowCredentials is true, allowedOrigins cannot contain the special value “*” since that cannot be set on the “Access-Control-Allow-Origin” response header. To allow credentials to a set of origins, list them explicitly or consider using “allowedOriginPatterns” instead.

2.2.3. 代理

代理法就是讓不同源的請求程式設計同源,比如靜態頁面是http://a.com/index.html,動態ajax請求是http://b.com/api/***,那麼將對應的服務部署在不同的機器上,然後使用一個公共域名,如:c.com,作為nginx反向代理的入口,再將靜態服務和動態服務分別掛在後面的被代理區域網伺服器內即可

server{
    listen:80;
    server_name: c.com;
    #靜態資源
    location /{
        proxy_pass http://localhost:8080/;
    }
    #ajax動態請求
    location /api{
        proxy_pass http://localhost:8081/;
    }
}