重拾後端之Spring Boot(五) -- 跨域、自定義查詢及分頁

重拾後端之Spring Boot(一):REST API的搭建能夠這樣簡單
重拾後端之Spring Boot(二):MongoDb的無縫集成
重拾後端之Spring Boot(三):找回熟悉的Controller,Service
重拾後端之Spring Boot(四):使用 JWT 和 Spring Security 保護 REST API
重拾後端之 Spring Boot(五) -- 跨域、自定義查詢及分頁
juejin.im/post/58f9ab…
重拾後端之Spring Boot(六) -- 熱加載、容器和多項目css

跨域

前面咱們初步作出了一個能夠實現受保護的 REST API,可是咱們沒有涉及一個前端領域很重要的問題,那就是跨域請求( cross-origin HTTP request )。先來回顧一些背景知識:html

跨域請求

定義:當咱們從自己站點請求不一樣域名或端口的服務所提供的資源時,就會發起跨域請求。前端

例如最多見的咱們不少的 css 樣式文件是會連接到某個公共 CDN 服務器上,而不是在自己的服務器上,這其實就是典型的一個跨域請求。但瀏覽器因爲安全緣由限制了在腳本( script )中發起的跨域 HTTP 請求。也就是說 XMLHttpRequestFetch 等是遵循「同源規則」的,即只能訪問本身服務器的指定端口的資源(同一服務器不一樣端口也會視爲跨域)。但這種限制在今天,咱們的應用須要訪問多種外部 API 或 資源的時候就不能知足開發者的需求了,所以就產生了若干對於跨域的解決方案,JSONP 是其中一種,但在今天來看主流的更完全的解決方案是 CORS ( Cross-Origin Resource Sharing )。java

跨域資源共享 ( CORS )

這種機制將跨域的訪問控制權交給服務器,這樣能夠保證安全的跨域數據傳輸。現代瀏覽器通常會將 CORS 的支持封裝在 HTTP API 之中( 好比 XMLHttpRequestFetch ),這樣能夠有效控制使用跨域請求的風險,由於你繞不過去,總得要使用 API 吧。git

歸納來講,這個機制是增長一系列的 HTTP 頭來讓服務器能夠描述哪些源是容許使用瀏覽器來訪問資源的。並且對於簡單的請求和複雜請求,處理機制是不同的。github

簡單請求僅容許三個 HTTP 方法:GET,POST 以及 HEAD,另外只能支持若干 header 參數:Accept , Accept-Language , Content-Language , Content-Type (值只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain), DPR , Downlink , Save-Data , Viewport-Width 和 Width。web

對於簡單請求來講,好比下面這樣一個簡單的GET請求:從 http://me.domain 發起到 http://another.domain/data/blablabla 的資源請求spring

GET /data/blablabla/ HTTP/1.1
// 請求的域名
Host: another.domain
...//省略其它部分,重點是下面這句,說明了發起請求者的來源
Origin: http://me.domain複製代碼

應用了 CORS 的對方服務器返回的響應應該像下面這個樣子,固然這裏 Access-Control-Allow-Origin: * 中的 * 表示任何網站均可以訪問該資源,若是要限制只能從 me.domain 訪問,那麼須要改爲 Access-Control-Allow-Origin: http://me.domain數據庫

HTTP/1.1 200 OK
...//省略其它部分
Access-Control-Allow-Origin: *
...//省略其它部分
Content-Type: application/json複製代碼

那麼對於複雜請求怎麼辦呢?這須要一次預檢請求和一次實際的請求,也就是說須要兩次和對方服務器的請求/響應。預檢請求是以 OPTION 方法進行的,由於 OPTION 方法不會改變任何資源,因此這個預檢請求是安全的,它的職責在於發送實際請求將會使用的 HTTP 方法以及將要發送的 HEADER 中將攜帶哪些內容,這樣對方服務器能夠根據預檢請求的信息決定是否接受。編程

// 預檢請求
OPTIONS /resources/post/ HTTP/1.1
Host: another.domain
...// 省略其它部分
Origin: http://me.domain
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type複製代碼

服務器對預檢請求的響應以下:

HTTP/1.1 200 OK
// 省略其它部分
Access-Control-Allow-Origin: http://me.domain
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
// 省略其它部分
Content-Type: text/plain複製代碼

接下來的正式請求就和上面的簡單請求差很少了,就不贅述了。

在 Spring Boot 中如何啓用 CORS

囉嗦了這麼多,終於進入正題,但我一直以爲不能光知其然而不知其因此然,因此各位就忍了吧。加入 CORS 的支持在 Spring Boot 中簡單到不忍直視,添加一個配置類便可:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 設置你要容許的網站域名,若是全容許則設爲 *
        config.addAllowedOrigin("http://localhost:4200");
        // 若是要限制 HEADER 或 METHOD 請自行更改
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        // 這個順序很重要哦,爲避免麻煩請設置在最前
        bean.setOrder(0);
        return bean;
    }
}複製代碼

若是咱們使用 POSTMAN 訪問一下 API,會發現獲得一個 Invalid CORS request 的響應,由於咱們的 API 只受權給了 localhost:4200

用 POSTMAN 沒法獲得請求結果
用 POSTMAN 沒法獲得請求結果

固然,若是咱們使用 CURL 的話是能夠訪問的,這是由於 CURL 不是瀏覽器。

嗯嗯,這樣就結束了,這節好水,但就是這麼簡單啊。

自定義查詢

咱們回過頭來再來看看數據查詢,大部分狀況下 Spring Data 提供的按方法名進行查詢的方式足夠簡單也足夠強大,但總歸仍是有不少侷限。爲了說明這個問題,也順便爲個人新 Angular 項目打造一個 API,咱們把 API 的需求改一下。如今咱們要作的不是一個簡單的 Todo 了,而是相似 Teambition 或 Worktile 那樣的企業協做平臺,固然咱們不會作的那麼複雜,是個簡化版本。那麼這時咱們的對象模型是這樣的:

主要的領域對象
主要的領域對象

咱們首先看一下 Project 這個對象,咱們來構建它的 API,增刪改沒啥可講。可是查詢上會有點不同,首先咱們並不但願把全部的 Project 都查出來,而是該用戶參與的項目要提供一個 API 給客戶端。

Project 和 User 按關係型數據庫的見解是個多對多的關係,在MongoDB中這其實有多種作法,能夠在 User 對象中設置一個 Project 的集合屬性,也能夠在 Project 中設置 User 的集合屬性 (在咱們的例子裏是 memebers ),還能夠二者結合,就是在 User 和 Projet 中互相有對方的集合屬性。具體採用哪一種須要看業務場景和性能需求,好比若是任何一個項目的成員數若是不會很大,那麼在 Project 中嵌入 User 集合就比較划算;若是項目的成員較多,但一個成員歸屬的項目不會不少的狀況下,就能夠把 Project 的集合嵌入到 User 中。咱們這裏採用了第一種作法。

那麼接下來咱們來寫該用戶參與的項目的查詢。固然咱們能夠按照 Spring Data 強大的按方法名稱來生成對應查詢的方式來作:尋找 members 屬性中包含該用戶的集合

Set<Project> findByMembersContaining(User user)複製代碼

看起來還能夠,挺簡單,可是若是咱們說再加兩個條件要篩選 project.enabled == true (咱們不會物理刪除項目,而是設置其標誌位 enabled,因此這就是篩選未刪除的項目) 和 project.archived == false (項目完結後須要歸檔,這就是篩選未歸檔的)。這兩個條件一加上,好傢伙,咱們的方法名變成了下面這個樣子,不忍直視啊:

Set<Project> findByMembersContainingAndEnabledAndArchived(User user, boolean enabled, boolean archived)複製代碼

固然好用仍是好用了,可是這個方法名也太長了,好在 Spring Data 中提供不少種方式自定義查詢,咱們介紹一種相對簡單的:利用 @Query 註解來進行查詢,方法名字就沒有那麼雷人了:

@Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}")
Set<Project> findRelated(User user, boolean enabled, boolean archived)複製代碼

這個註解中的內容是一個 JSON 對象,就和咱們在 MongoDB 的控制檯查詢的find()中的內容是同樣的,只不過將雙引號換成單引號,將須要變量用 [0][1][2] 的形式表示第1、第二和第三個參數。那麼 ?#{} 是表示裏面的內容是個 SpEL ( Spring 的表達式語言) 表達式。

因此實踐中,咱們能夠在 MongoDB 的控制檯去實驗語句是否好用,而後在 Spring 中編寫表達式。

db.project.find(
    {
        "owner.$id": ObjectId("58f5a904edc76ab0e033cfc3"),        
        "enabled": true, 
        "archived": false
    })複製代碼

在MongoDB的console查詢
在MongoDB的console查詢

數據的分頁

不少時候咱們但願 API 返回的數據是能夠分頁的,這個分頁問題在 Spring Boot 有怎樣便捷的方法呢?咱們是否須要再定義一堆什麼 pageSize,pageCount,start, off 的參數呢?答案是徹底不必,分頁這個事情對於 Spring Boot 來講很簡單,只需改變各層級原有方法中返回的 List 或 Set 對象爲 Page 對象,傳入參數多一個 Pageable 類型的參數便可。

// Controller
@RequestMapping(method = RequestMethod.GET)
public Page
  
  
  

 
  
  findRelated( @RequestHeader(value = "userId") String userId, @RequestParam(value = "enabled", defaultValue = "true", required = false) boolean enabled, @RequestParam(value = "archived", defaultValue = "false", required = false) boolean archived, Pageable pageable) { return service.findRelated(userId, enabled, archived, pageable); } // Repository @Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}") Page 
 
  
    findRelated(ObjectId userId, boolean enabled, boolean archived, Pageable pageable); 
   

 複製代碼

如今呢,咱們就能夠這樣使用了 GET http://localhost:8090/projects/?page=0&size=3 表示取每頁三個數據取第一頁。

本章代碼:github.com/wpcfan/spri…

另外,個人 《Angular 從零到一》出版了,下面是書籍的內容簡介:

本書系統介紹Angular的基礎知識與開發技巧,可幫助前端開發者快速入門。共有9章,第1章介紹Angular的基本概念,第2~7章從零開始搭建一個待辦事項應用,而後逐步增長功能,如增長登陸驗證、將應用模塊化、多用戶版本的實現、使用第三方樣式庫、動態效果製做等。第8章介紹響應式編程的概念和Rx在Angular中的應用。第9章介紹在React中很是流行的Redux狀態管理機制,這種機制的引入可讓代碼和邏輯隔離得更好,在團隊工做中強烈建議採用這種方案。本書不只講解Angular的基本概念和最佳實踐,並且分享了做者解決問題的過程和邏輯,講解細膩,風趣幽默,適合有面向對象編程基礎的讀者閱讀。

歡迎你們圍觀、訂購、提出寶貴意見。

京東連接:item.m.jd.com/product/120…

Angular從零到一
Angular從零到一
相關文章
相關標籤/搜索