SpringBoot實現Java高併發秒殺系統之Web層開發(三)

優惠碼優惠發表於2018-11-07

SpringBoot實現Java高併發秒殺系統之Web層開發(三)

接著上一篇文章:SpringBoot實現Java高併發之Service層開發,今天我們開始講SpringBoot實現Java高併發秒殺系統之Web層開發。

Web層即Controller層,當然我們所說的都是在基於Spring框架的系統上而言的,傳統的SSH專案中,與頁面進行互動的是struts框架,但struts框架很繁瑣,後來就被SpringMVC給頂替了,SpringMVC框架在與頁面的互動上提供了更加便捷的方式,MVC的設計模式也是當前非常流行的一種設計模式。這次我們針對秒殺系統講解一下秒殺系統需要和頁面互動的操作和資料都涉及哪些?

本專案的原始碼請參看:springboot-seckill 如果覺得不錯可以star一下哦(#^.^#)

本專案一共分為四個模組來講解,具體的開發教程請看我的部落格文章:

  • SpringBoot實現Java高併發秒殺系統之DAO層開發(一)

  • SpringBoot實現Java高併發秒殺系統之Service層開發(二)

  • SpringBoot實現Java高併發秒殺系統之Web層開發(三)

  • SpringBoot實現Java高併發秒殺系統之併發優化(四)

首先如果你對SpringBoot專案還是不清楚的話,我依然推薦你看一下我的這個專案:優雅的入門SpringBoot2.x,整合Mybatis實現CRUD

前端互動流程設計

編寫Controller就是要搞清楚:1.頁面需要什麼資料?2.頁面將返回給Controller什麼資料?3.Controller應該返回給頁面什麼資料?

帶著這些問題我們看一下秒殺詳情頁流程邏輯(不再講基本的findByIdfindAll()方法):

因為整個秒殺系統中最核心的業務就是:1.減庫存;2.查詢訂單明細。我們看一下Controller層的原始碼:

@Controller
@RequestMapping("/seckill")
public class SeckillController {

@Autowired
private SeckillService seckillService;

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@ResponseBody
@RequestMapping("/findAll")
public List<Seckill> findAll() {
return seckillService.findAll();
}

@ResponseBody
@RequestMapping("/findById")
public Seckill findById(@RequestParam("id") Long id) {
return seckillService.findById(id);
}

@RequestMapping("/{seckillId}/detail")
public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
if (seckillId == null) {
return "page/seckill";
}
Seckill seckill = seckillService.findById(seckillId);
model.addAttribute("seckill", seckill);
if (seckill == null) {
return "page/seckill";
}
return "page/seckill_detail";
}

@ResponseBody
@RequestMapping(value = "/{seckillId}/exposer",
method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId) {
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}
return result;
}

@RequestMapping(value = "/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@RequestParam("money") BigDecimal money,
@CookieValue(value = "killPhone", required = false) Long userPhone) {
if (userPhone == null) {
return new SeckillResult<SeckillExecution>(false, "未註冊");
}
try {
SeckillExecution execution = seckillService.executeSeckill(seckillId, money, userPhone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e) {
SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true, seckillExecution);
} catch (SeckillCloseException e) {
SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(true, seckillExecution);
} catch (SeckillException e) {
SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true, seckillExecution);
}
}

@ResponseBody
@GetMapping(value = "/time/now")
public SeckillResult<Long> time() {
Date now = new Date();
return new SeckillResult(true, now.getTime());
}
}

下面我以問答的形式講解一下Controller層方法的定義:

1.@ResponseBody@RestController註解分別有什麼作用?

  • @ResponseBody註解標識的方法,Spring會將此方法return的資料轉換成JSON格式且不會被Spring檢視解析器所掃描到,也就是此方法永不可能返回一個檢視頁面。且這個註解只能用在方法體上,不能用在類上。

  • @RestController註解標識的類,Spring會將其下的所有方法return的資料都轉換成JSON格式且不會被Spring檢視解析器掃描到,也就是此類下面的所有方法都不可能返回一個檢視頁面。且這個註解只能用在類上,不能用在方法體上。

2.@RequestMapping{xx}的語法是什麼?@PathVariable註解的用處是什麼?

Spring框架很早就支援開發REST資源。也是就是現在我們定義的RESTful URL,在Spring框架上支援的尤為完美,我們可以在Controller中定義這樣一個URL對映地址:/{id}/detail,他是合理的RESTful URL定義方式。

這種URL的特點:URL地址由動態的資料拼接組成的,而不是將所有的資源全部對映到一個路徑下,比如:/article/detail

這種URL結構的優勢:我們能很容易從URL地址上判斷出該地址所展示的頁面是什麼?比如:/1/detail就可能表示ID為1的文章的詳情頁,看起來設計的很清晰。

這種URL如何進行互動:我們定義了/{id}/detail這樣一個URL對映地址,其對應的對映方法上就應該新增@PathVariable註解標識,如:@PathVariable("id") Long idSpring就能裝配前端傳遞的URL中指定位置的資料並賦值給id這個引數。比如前端呼叫後端介面:localhost:8080/seckill/1/detail,後端存在一個對映方法:@RequestMapping("/{id}/detail"),這樣就能剛好匹配上這個URL對映地址。

所以我們看一下秒殺系統的RESTful URL設計:

3.為什麼要單獨寫一個介面用來獲取當前系統時間?

由於我們開發的系統肯定不是給自己用的,我們的使用者可能處於不同的時區,他們的當前系統時間也是不同的,所以我們寫一個通用的時間規範:就是當前伺服器的時間。

4.SeckillResult是什麼?

在前面我們將Service層系統開發的時候就手動建立了很多類來封裝一些通用的結果資訊。而對於Controller層也會返回很多結果資料,比如傳入的URL中id值為null,那麼就沒必要繼續向下請求,而是直接給頁面返回false資訊。

於是我們建立:SeckillResult.java

public class SeckillResult<T> {

private boolean success;

private T data;

private String error;

public SeckillResult(boolean success, T data) {
this.success = success;
this.data = data;
}

public SeckillResult(boolean success, String error) {
this.success = success;
this.error = error;
}
}

泛型T表示可以代表不同型別的物件。這是泛型類應用很廣泛的一個特性,我們呼叫SeckillResult類,將其中的T用什麼替換那麼T就表示這個替換的物件型別。

頁面設計

用了哪些技術?

  1. HTML頁面,用Bootstrap繪製。
  2. Thymeleaf模板引擎渲染HTML頁面,使得HTML頁面擁有類似JSP頁面一樣功能。
  3. JS方面使用原生的JQuery。

我們來看一下前端的頁面設計:

本專案使用Cookie儲存使用者手機號的方式模擬使用者登入功能,實際上沒有與後端互動的操作。如果使用者沒有登入就開啟了商品詳情頁會直接彈出一個手機號登入框提醒使用者登入,且沒有登入時無法關閉登入框的。

具體的原始碼請看:GitHub

思考

在從JSP頁面轉換到HTML頁面的時候我常會遇到這麼一個問題:前端如何取出來後端查詢到的資料?

在之前我們寫的JSP頁面中,可以通過將後端查詢到的資料放進request,session域物件中,JSP頁面可以直接呼叫Java中域物件的資料,甚至可以通過EL表示式(${})來直接獲取引數,但是這種方法有一個弊端:Controller必須是返回一個檢視,這樣才能在此檢視中獲取存進域物件中的資料。

而我們現在都開始用HTML頁面,也無法從域物件中取出資料該怎麼辦呢?我這裡提供兩個思路:

  • 1.像本專案中一樣,前端使用Thymeleaf模板引擎渲染頁面,那麼Thymeleaf內建很多方法如同JSP頁面的EL表示式。Thymeleaf在HTML中取出域物件資料使用:<span th:text="${xx}">;在JS中取出域物件資料:var v = [[${xx}]](當然都必須是在HTML頁面中,在外部JS檔案中是得不到資料的)。

  • 2.使用原生js提供的location物件,我們先看一下URL的組成結構:

詳細介紹請看:博文

舉個例子

function QueryUrl(name){
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r!=null)return unescape(r[2]); return null;
}

// 呼叫方法
alert(QueryUrl("引數名1"));


相關文章