SpringMVC開(kāi)啟CORS支持

前言

瀏覽器出于安全考慮崔赌,限制了JS發(fā)起跨站請(qǐng)求,使用XHR對(duì)象發(fā)起請(qǐng)求必須遵循同源策略(SOP:Same Origin Policy),跨站請(qǐng)求會(huì)被瀏覽器阻止揣苏,這對(duì)開(kāi)發(fā)者來(lái)說(shuō)是很痛苦的一件事,尤其是要開(kāi)發(fā)前后端分離的應(yīng)用時(shí)件舵。

在現(xiàn)代化的Web開(kāi)發(fā)中卸察,不同網(wǎng)絡(luò)環(huán)境下的資源數(shù)據(jù)共享越來(lái)越普遍,同源策略可以說(shuō)是在一定程度上限制了Web API的發(fā)展铅祸。

簡(jiǎn)單的說(shuō)坑质,CORS就是為了AJAX能夠安全跨域而生的合武。至于CORS的安全性研究,本文不做探討涡扼。


目錄

  1. CORS淺述

  2. 如何使用稼跳?CORS的HTTP頭

  3. 初始項(xiàng)目準(zhǔn)備

  4. CorsFilter: 過(guò)濾器階段的CORS

  5. CorsInterceptor: 攔截器階段的CORS

  6. @CrossOrigin:Handler階段的CORS

  7. 小結(jié)

  8. 追求極致的開(kāi)發(fā)體驗(yàn):整合第三方CORSFilter

  9. 示例代碼下載


CORS淺述

名詞解釋:跨域資源共享(Cross-Origin Resource Sharing)

概念:是一種跨域機(jī)制、規(guī)范吃沪、標(biāo)準(zhǔn)汤善,怎么叫都一樣,但是這套標(biāo)準(zhǔn)是針對(duì)服務(wù)端的票彪,而瀏覽器端只要支持HTML5即可红淡。

作用:可以讓服務(wù)端決定哪些請(qǐng)求源可以進(jìn)來(lái)拿數(shù)據(jù),所以服務(wù)端起主導(dǎo)作用(所以出了事找后臺(tái)程序猿降铸,無(wú)關(guān)前端^ ^)

常用場(chǎng)景:

  • 前后端完全分離的應(yīng)用在旱,比如Hybrid App
  • 開(kāi)放式只讀API,JS能夠自由訪問(wèn)推掸,比如地圖桶蝎、天氣、時(shí)間……

如何使用谅畅?CORS的HTTP頭

要實(shí)現(xiàn)CORS跨域其實(shí)非常簡(jiǎn)單登渣,說(shuō)白了就是在服務(wù)端設(shè)置一系列的HTTP頭,主要分為請(qǐng)求頭和響應(yīng)頭铃彰,在請(qǐng)求和響應(yīng)時(shí)加上這些HTTP頭即可輕松實(shí)現(xiàn)CORS

請(qǐng)求頭和響應(yīng)頭信息都是在服務(wù)端設(shè)置好的绍豁,一般在Filter階段設(shè)置,瀏覽器端不用關(guān)心牙捉,唯一要設(shè)置的地方就是:跨域時(shí)是否要攜帶cookie

  • HTTP請(qǐng)求頭:
#請(qǐng)求域
Origin: ”http://localhost:3000“

#這兩個(gè)屬性只出現(xiàn)在預(yù)檢請(qǐng)求中竹揍,即OPTIONS請(qǐng)求
Access-Control-Request-Method: ”P(pán)OST“
Access-Control-Request-Headers: ”content-type“
  • HTTP響應(yīng)頭:
#允許向該服務(wù)器提交請(qǐng)求的URI,*表示全部允許邪铲,在SpringMVC中芬位,如果設(shè)成*,會(huì)自動(dòng)轉(zhuǎn)成當(dāng)前請(qǐng)求頭中的Origin
Access-Control-Allow-Origin: ”http://localhost:3000“

#允許訪問(wèn)的頭信息
Access-Control-Expose-Headers: "Set-Cookie"

#預(yù)檢請(qǐng)求的緩存時(shí)間(秒)带到,即在這個(gè)時(shí)間段里昧碉,對(duì)于相同的跨域請(qǐng)求不會(huì)再預(yù)檢了
Access-Control-Max-Age: ”1800”

#允許Cookie跨域,在做登錄校驗(yàn)的時(shí)候有用
Access-Control-Allow-Credentials: “true”

#允許提交請(qǐng)求的方法揽惹,*表示全部允許
Access-Control-Allow-Methods:GET,POST,PUT,DELETE,PATCH

初始項(xiàng)目準(zhǔn)備

  • 補(bǔ)充一下被饿,對(duì)于簡(jiǎn)單跨域和非簡(jiǎn)單跨域,可以這么理解:
  1. 簡(jiǎn)單跨域就是GET搪搏,HEAD和POST請(qǐng)求狭握,但是POST請(qǐng)求的"Content-Type"只能是application/x-www-form-urlencoded, multipart/form-data 或 text/plain
  2. 反之,就是非簡(jiǎn)單跨域疯溺,此跨域有一個(gè)預(yù)檢機(jī)制论颅,說(shuō)直白點(diǎn)哎垦,就是會(huì)發(fā)兩次請(qǐng)求,一次OPTIONS請(qǐng)求恃疯,一次真正的請(qǐng)求

  • 首先新建一個(gè)靜態(tài)web項(xiàng)目漏设,定義三種類型的請(qǐng)求:簡(jiǎn)單跨域請(qǐng)求,非簡(jiǎn)單跨域請(qǐng)求今妄,帶Cookie信息的請(qǐng)求(做登錄校驗(yàn))郑口。代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>跨域demo</title>
    <link rel="stylesheet" href="node_modules/amazeui/dist/css/amazeui.min.css">
</head>

<body class="am-container">
<!--簡(jiǎn)單跨域-->
<button class="am-btn am-btn-primary" onclick="getUsers(this)">
    簡(jiǎn)單跨域: 獲取用戶列表
</button>
<p class="am-text-danger"></p>

<!--非簡(jiǎn)單跨域-->
<button class="am-btn am-btn-primary" onclick="addUser(this)">
    非簡(jiǎn)單跨域: 添加用戶(JSON請(qǐng)求)
</button>
<input type="text" placeholder="用戶名">
<p class="am-text-danger"></p>

<!--檢查是否登錄-->
<button class="am-btn am-btn-primary am-margin-right" onclick="checkLogin(this)">
    登錄校驗(yàn)
</button>
<p class="am-text-danger"></p>

<!--登錄-->
<button class="am-btn am-btn-primary" onclick="login(this)">
    登錄
</button>
<input type="text" placeholder="用戶名">
<p class="am-text-danger"></p>
</body>
<script src="node_modules/jquery/dist/jquery.min.js"></script>
<script src="node_modules/amazeui/dist/js/amazeui.js"></script>
<script>
    function getUsers(btn) {
        var $btn = $(btn);
        $.ajax({
            type: 'get',
            url: 'http://localhost:8080/api/users',
            contentType: "application/json;charset=UTF-8"
        }).then(
                function (obj) {
                    $btn.next('p').html(JSON.stringify(obj));
                },
                function () {
                    $btn.next('p').html('error...');
                }
        )
    }

    function addUser(btn) {
        var $btn = $(btn);
        var name = $btn.next('input').val();
        if (!name) {
            $btn.next('input').next('p').html('用戶名不能為空');
            return;
        }
        $.ajax({
            type: 'post',
            url: 'http://localhost:8080/api/users',
            contentType: "application/json;charset=UTF-8",
            data: name,
            dataType: 'json'
        }).then(
                function (obj) {
                    $btn.next('input').next('p').html(JSON.stringify(obj));
                },
                function () {
                    $btn.next('input').next('p').html('error...');
                }
        )
    }

    function checkLogin(btn) {
        var $btn = $(btn);
        $.ajax({
            type: 'get',
            url: 'http://localhost:8080/api/user/login',
            contentType: "application/json;charset=UTF-8",
            xhrFields: {
                withCredentials: true
            }
        }).then(
                function (obj) {
                    $btn.next('p').html(JSON.stringify(obj));
                },
                function () {
                    $btn.next('p').html('error...');
                }
        )
    }

    function login(btn) {
        var $btn = $(btn);
        var name = $btn.next('input').val();
        if (!name) {
            $btn.next('input').next('p').html('用戶名不能為空');
            return;
        }
        $.ajax({
            type: 'post',
            url: 'http://localhost:8080/api/user/login',
            contentType: "application/json;charset=UTF-8",
            data: name,
            dataType: 'json',
            xhrFields: {
                withCredentials: true
            }
        }).then(
                function (obj) {
                    $btn.next('input').next('p').html(JSON.stringify(obj));
                },
                function () {
                    $btn.next('input').next('p').html('error...');
                }
        )
    }
</script>
</html>
  • 然后啟動(dòng)web項(xiàng)目(這里推薦一個(gè)所見(jiàn)即所得工具:browser-sync)
browser-sync start --server --files "*.html"


  • 接來(lái)下,做服務(wù)端的事情蛙奖,新建一個(gè)SpringMVC項(xiàng)目潘酗,這里推薦一個(gè)自動(dòng)生成Spring種子項(xiàng)目的網(wǎng)站:http://start.spring.io/

    種子項(xiàng)目
    種子項(xiàng)目

  • 項(xiàng)目結(jié)構(gòu)如下:


    項(xiàng)目結(jié)構(gòu)
    項(xiàng)目結(jié)構(gòu)
  • 在pom.xml中引入lombok和guava

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.8</version>
</dependency>
  • 模擬數(shù)據(jù)源:UserDB
public class UserDB {

    public static Cache<String, User> userdb = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build();

    static {
        String id1 = UUID.randomUUID().toString();
        String id2 = UUID.randomUUID().toString();
        String id3 = UUID.randomUUID().toString();
        userdb.put(id1, new User(id1, "jear"));
        userdb.put(id2, new User(id2, "tom"));
        userdb.put(id3, new User(id3, "jack"));
    }
}
  • 編寫(xiě)示例控制器:UserController
@RestController
@RequestMapping("/users")
public class UserController {

    @RequestMapping(method = RequestMethod.GET)
    List<User> getList() {
        return Lists.newArrayList(userdb.asMap().values());
    }

    @RequestMapping(method = RequestMethod.POST)
    List<String> add(@RequestBody String name) {
        if (userdb.asMap().values().stream().anyMatch(user -> user.getName().equals(name))) {
            return Lists.newArrayList("添加失敗, 用戶名'" + name + "'已存在");
        }
        String id = UUID.randomUUID().toString();
        userdb.put(id, new User(id, name));
        return Lists.newArrayList("添加成功: " + userdb.getIfPresent(id));
    }
}
  • 編寫(xiě)示例控制器:UserLoginController
@RestController
@RequestMapping("/user/login")
public class UserLoginController {

    @RequestMapping(method = RequestMethod.GET)
    Object getInfo(HttpSession session) {
        Object object = session.getAttribute("loginer");
        return object == null ? Lists.newArrayList("未登錄") : object;
    }

    @RequestMapping(method = RequestMethod.POST)
    List<String> login(HttpSession session, @RequestBody String name) {
        Optional<User> user = userdb.asMap().values().stream().filter(user1 -> user1.getName().equals(name)).findAny();
        if (user.isPresent()) {
            session.setAttribute("loginer", user.get());
            return Lists.newArrayList("登錄成功!");
        }
        return Lists.newArrayList("登錄失敗, 找不到用戶名:" + name);
    }
}
  • 最后啟動(dòng)服務(wù)端項(xiàng)目
mvn clean package
debug模式啟動(dòng)Application


  • 到這里,主要工作都完成了雁仲,打開(kāi)瀏覽器,訪問(wèn)靜態(tài)web項(xiàng)目琐脏,打開(kāi)控制臺(tái)攒砖,發(fā)現(xiàn)Ajax請(qǐng)求無(wú)法獲取數(shù)據(jù),這就是同源策略的限制
  • 下面我們一步步來(lái)開(kāi)啟服務(wù)端的CORS支持

CorsFilter: 過(guò)濾器階段的CORS

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        // 對(duì)響應(yīng)頭進(jìn)行CORS授權(quán)
        MyCorsRegistration corsRegistration = new MyCorsRegistration("/**");
        corsRegistration.allowedOrigins(CrossOrigin.DEFAULT_ORIGINS)
                .allowedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name(), HttpMethod.PUT.name())
                .allowedHeaders(CrossOrigin.DEFAULT_ALLOWED_HEADERS)
                .exposedHeaders(HttpHeaders.SET_COOKIE)
                .allowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS)
                .maxAge(CrossOrigin.DEFAULT_MAX_AGE);

        // 注冊(cè)CORS過(guò)濾器
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
        configurationSource.registerCorsConfiguration("/**", corsRegistration.getCorsConfiguration());
        CorsFilter corsFilter = new CorsFilter(configurationSource);
        return new FilterRegistrationBean(corsFilter);
    }
}
  • 現(xiàn)在測(cè)試一下“簡(jiǎn)單跨域”和“非簡(jiǎn)單跨域”日裙,已經(jīng)可以正常響應(yīng)了


    瀏覽器圖片
    瀏覽器圖片
  • 再來(lái)測(cè)試一下 “登錄校驗(yàn)” 和 “登錄”吹艇,看看cookie是否能正常跨域


    瀏覽器圖片
    瀏覽器圖片
  • 如果把服務(wù)端的allowCredentials設(shè)為false昂拂,或者ajax請(qǐng)求中不帶{withCredentials: true},那么登錄校驗(yàn)永遠(yuǎn)都是未登錄,因?yàn)閏ookie沒(méi)有在瀏覽器和服務(wù)器之間傳遞


CorsInterceptor: 攔截器階段的CORS

既然已經(jīng)有了Filter級(jí)別的CORS忘瓦,為什么還要CorsInterceptor呢吱晒?因?yàn)榭刂屏6炔灰粯樱ilter是任意Servlet的前置過(guò)濾器联四,而Inteceptor只對(duì)DispatcherServlet下的請(qǐng)求攔截有效撑碴,它是請(qǐng)求進(jìn)入Handler的最后一道防線,如果再設(shè)置一層Inteceptor防線朝墩,可以增強(qiáng)安全性和可控性醉拓。

關(guān)于這個(gè)階段的CORS,不得不吐槽幾句收苏,Spring把CorsInteceptor寫(xiě)死在了攔截器鏈上的最后一個(gè)亿卤,也就是說(shuō)如果我有自定義的Interceptor,請(qǐng)求一旦被我自己的攔截器攔截下來(lái)鹿霸,則只能通過(guò)CorsFilter授權(quán)跨域排吴,壓根走不到CorsInterceptor,至于為什么杜跷,下面會(huì)講到傍念。

所以說(shuō)CorsInterceptor是專為授權(quán)Handler中的跨域而寫(xiě)的矫夷。

廢話不多說(shuō),直接上代碼:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public FilterRegistrationBean corsFilterRegistrationBean() {
        // 對(duì)響應(yīng)頭進(jìn)行CORS授權(quán)
        MyCorsRegistration corsRegistration = new MyCorsRegistration("/**");
        this._configCorsParams(corsRegistration);

        // 注冊(cè)CORS過(guò)濾器
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
        configurationSource.registerCorsConfiguration("/**", corsRegistration.getCorsConfiguration());
        CorsFilter corsFilter = new CorsFilter(configurationSource);
        return new FilterRegistrationBean(corsFilter);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 配置CorsInterceptor的CORS參數(shù)
        this._configCorsParams(registry.addMapping("/**"));
    }

    private void _configCorsParams(CorsRegistration corsRegistration) {
        corsRegistration.allowedOrigins(CrossOrigin.DEFAULT_ORIGINS)
                .allowedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name(), HttpMethod.PUT.name())
                .allowedHeaders(CrossOrigin.DEFAULT_ALLOWED_HEADERS)
                .exposedHeaders(HttpHeaders.SET_COOKIE)
                .allowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS)
                .maxAge(CrossOrigin.DEFAULT_MAX_AGE);
    }
}
  • 打開(kāi)瀏覽器憋槐,效果和上面一樣

@CrossOrigin:Handler階段的CORS

如果把前面的代碼認(rèn)真寫(xiě)一遍双藕,應(yīng)該已經(jīng)發(fā)現(xiàn)這個(gè)注解了,這個(gè)注解是用在控制器方法上的阳仔,其實(shí)Spring在這里用的還是CorsInterceptor忧陪,做最后一層攔截,這也就解釋了為什么CorsInterceptor永遠(yuǎn)是最后一個(gè)執(zhí)行的攔截器近范。

這是最小控制粒度了嘶摊,可以精確到某個(gè)請(qǐng)求的跨域控制

// 先把WebConfig中前兩階段的配置注釋掉,再到這里加跨域注解
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping(method = RequestMethod.GET)
List<User> getList() {
    return Lists.newArrayList(userdb.asMap().values());
}
  • 打開(kāi)瀏覽器评矩,發(fā)現(xiàn)只有第一個(gè)請(qǐng)求可以正骋抖眩跨域


    Handler跨域
    Handler跨域

小結(jié)

三個(gè)階段的CORS配置順序是后面疊加到前面,而不是后面完全覆蓋前面的斥杜,所以在設(shè)計(jì)的時(shí)候虱颗,每個(gè)階段如何精確控制CORS,還需要在實(shí)踐中慢慢探索……


追求更好的開(kāi)發(fā)體驗(yàn):整合第三方CORSFilter

  • 對(duì)這個(gè)類庫(kù)的使用和分析將在下一篇展開(kāi)

  • 官網(wǎng):http://software.dzhuvinov.com/cors-filter.html

  • 喜歡用這個(gè)CORSFilter主要是因?yàn)樗С諧ORS配置文件蔗喂,能夠自動(dòng)讀取classpath下的cors.properties忘渔,還有file watching的功能


示例代碼下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市缰儿,隨后出現(xiàn)的幾起案子畦粮,更是在濱河造成了極大的恐慌,老刑警劉巖乖阵,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宣赔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡义起,警方通過(guò)查閱死者的電腦和手機(jī)拉背,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)默终,“玉大人椅棺,你說(shuō)我怎么就攤上這事∑氡危” “怎么了两疚?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)含滴。 經(jīng)常有香客問(wèn)我诱渤,道長(zhǎng),這世上最難降的妖魔是什么谈况? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任勺美,我火速辦了婚禮递胧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赡茸。我一直安慰自己缎脾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布占卧。 她就那樣靜靜地躺著遗菠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪华蜒。 梳的紋絲不亂的頭發(fā)上辙纬,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音叭喜,去河邊找鬼贺拣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捂蕴,可吹牛的內(nèi)容都是我干的纵柿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼启绰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了沟使?” 一聲冷哼從身側(cè)響起委可,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腊嗡,沒(méi)想到半個(gè)月后着倾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡燕少,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年卡者,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片客们。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崇决,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出底挫,到底是詐尸還是另有隱情恒傻,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布建邓,位于F島的核電站盈厘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏官边。R本人自食惡果不足惜沸手,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一外遇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧契吉,春花似錦跳仿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至租悄,卻和暖如春谨究,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泣棋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工胶哲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人潭辈。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓鸯屿,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親把敢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寄摆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)修赞,斷路器婶恼,智...
    卡卡羅2017閱讀 134,654評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,110評(píng)論 25 707
  • 引用自HTTP訪問(wèn)控制(CORS) 當(dāng) Web 資源請(qǐng)求由其它域名或端口提供的資源時(shí),會(huì)發(fā)起跨域 HTTP 請(qǐng)求(...
    有涯逐無(wú)涯閱讀 2,586評(píng)論 0 4
  • 我不是一個(gè)愛(ài)蹭熱點(diǎn)的人,但對(duì)于最近熱傳的“馬化騰朋友圈怒懟朱嘯虎”事件割择,我想從不同的視角眷篇,聊聊我對(duì)這事背后的想法。...
    產(chǎn)品志異閱讀 1,049評(píng)論 4 3
  • 賈雨村是個(gè)什么樣的人荔泳? 關(guān)于賈雨村這個(gè)人蕉饼,書(shū)中給了詳細(xì)的資料! 1.賈雨村的家世 紅樓夢(mèng)第一回形容他的第一個(gè)詞是“...
    淚花香閱讀 19,574評(píng)論 4 27