中級18 - Java Web:從零開始Spring Web

之前學(xué)習(xí)了 Java 的各種必備基礎(chǔ)知識荆萤,這一篇是一個(gè)分水嶺,開始真正從零創(chuàng)建一個(gè) Spring Web 項(xiàng)目称诗,可以運(yùn)用到生產(chǎn)環(huán)境中的那種蒸痹。

使用 Spring 進(jìn)行基本的 Java Web 開發(fā):

  • 創(chuàng)建和聲明 Service Bean
  • 創(chuàng)建和聲明 Controller Bean
  • 處理各種各樣的 HTTP 請求

1. 從零開始 Spring 應(yīng)用

從官網(wǎng) Building an Application with Spring Boot 抄即可。

  • pom.xml
  • src/main/java/hello/Application.java
  • src/main/java/hello/HelloController.java

添加 pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-boot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

創(chuàng)建基本的 Spring 目錄結(jié)構(gòu)虫碉,在 src/main/java 下面定義自己的包名:


image.png
image.png

創(chuàng)建一個(gè)入口類贾惦,以便讓 Spring 來啟動我們的程序:

package hello;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {

            System.out.println("Let's inspect the beans provided by Spring Boot:");

            String[] beanNames = ctx.getBeanDefinitionNames();
            Arrays.sort(beanNames);
            for (String beanName : beanNames) {
                System.out.println(beanName);
            }

        };
    }

}

現(xiàn)在在瀏覽器中請求 localhost:8080,已經(jīng)有響應(yīng)了敦捧,但是 404须板。
所以再創(chuàng)建一個(gè) Web Controller 作為一個(gè)簡單的 Web 應(yīng)用,該 Controller 中定義了一個(gè)可以用來處理路徑響應(yīng)并順便返回個(gè)字符串的方法兢卵。
也就是我們常說的前后端通信時(shí)的“接口”习瑰,科學(xué)點(diǎn)叫做“路徑”,這是 HTTP 協(xié)議中的概念:

package hello;

import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

    @RequestMapping("/")
    public String index() {
        return "Hello World";
    }
}

2. Web 應(yīng)用的本質(zhì)(HTTP)

處理 HTTP 請求:

  • 從 HTTP 請求中提取 query string (查詢字符串)
  • 從 HTTP 請求中接收 payload(負(fù)載/請求體/請求 body)中的參數(shù)

返回 HTTP 響應(yīng):

  • status code
  • HTTP response header
  • HTTP response body:JSON秽荤、HTML 等等

3. 從 GET 請求的查詢字符串中獲取參數(shù)

一條請求的參數(shù)可以來自于:

  • 查詢字符串:通常用來傳遞非敏感信息 ?name=tony&age=25
  • 請求路徑
  • 請求體

其實(shí)還有其他的請求甜奄,比如 POST 也可以在查詢字符串中攜帶參數(shù)。但實(shí)際應(yīng)用中王滤,如果用了 POST贺嫂,參數(shù)就可以全放在請求體中,而用不著放在請求路徑后面了雁乡。

使用 @RequestParam 處理查詢字符串參數(shù):

@RequestMapping("/search")
public String index(@RequestParam("q") String searchKeyword) {
    return "You are searching: " + searchKeyword;
}
image.png
image.png

可以繼續(xù)增加查詢參數(shù)第喳,并且可以指定為非必傳:

@RequestMapping("/search")
public String index(@RequestParam("q") String searchKeyword,
                    @RequestParam(value = "charset", required = false) String charset) {
    return "You are searching: " + searchKeyword + " " + charset;
}
image.png
image.png

4. RESTful API、HTTP method 與參數(shù)獲取

RESTful API 只是一種近年來流行的約定踱稍。
參考 MDN 中 HTTP 的方法動詞曲饱,以及 RESTful API 的業(yè)界標(biāo)桿 Github 的 REST API v3悠抹。

使用 HTTP 動詞來代表動作:

  • GET:獲取資源
  • POST:新建資源
  • PUT:更新資源
  • DELETE:刪除資源

使用 URL 名詞來代表資源:

  • 資源里面沒有動詞
  • 使用復(fù)數(shù)來代表資源列表

RESTful 風(fēng)格好處:

  • 清晰、優(yōu)雅扩淀、語義化
  • 方便批量自動創(chuàng)建接口
image.png
image.png

接下來實(shí)現(xiàn)一個(gè) Github 風(fēng)格的 API:

Unlock an issue:
Users with push access can unlock an issue's conversation.
DELETE /repos/:owner/:repo/issues/:issue_number/lock

可以繼續(xù)使用 @RequestMapping 注解楔敌,會把所有類型(動詞)的請求都映射到當(dāng)前方法中進(jìn)行處理。
可以為注解傳入?yún)?shù) method驻谆,限制為只處理 delete 請求卵凑。

或者,直接使用 @DeleteMapping(當(dāng)然胜臊,也可以同時(shí)配合 @RequestMapping 定義在類上來處理根路徑)勺卢。
Spring 會識別路徑中的參數(shù)并和方法的參數(shù)進(jìn)行綁定:

package hello;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.HashMap;

@RestController
@RequestMapping("repos") // 該 Controller 響應(yīng) repos 路徑及其子路徑的所有請求類型
public class IssueController {

    // DELETE /repos/:owner/:repo/issues/:issue_number/lock
    @DeleteMapping("{owner}/{repo}/issues/{issueNumber}/lock") // 只響應(yīng) delete 方法
//    @RequestMapping(
//            value = "{owner}/{repo}/issues/{issueNumber}/lock",
//            method = { RequestMethod.DELETE }
//    )
    public void unlock(
            @PathVariable("owner") String owner,
            @PathVariable("repo") String repo,
            @PathVariable("issueNumber") String issueNumber) {
        System.out.println(owner);
        System.out.println(repo);
        System.out.println(issueNumber);
    }

}

5. 從 POST 請求中獲取參數(shù)

從 HTTP POST 請求中提取 body:

場景 Content-Type 使用注解 適用于
提取整個(gè) body 中的對象 application/json @RequestBody JSON
提取 body 中的參數(shù) application/x-www-form-urlencoded @RequestParam 表單

Create an issue

POST /repos/:owner/:repo/issues

本例中,同時(shí)處理了路徑參數(shù)和請求體中的參數(shù)象对。

安裝插件黑忱,幫助把 json 參數(shù)變成有類型的 Java Bean,方便進(jìn)一步獲取 body 中的參數(shù)勒魔。否則 @RequestBody 默認(rèn)會把 json 處理成 LinkedHashMap 傳入進(jìn)來:

// POST /repos/:owner/:repo/issues
@PostMapping("/{owner}/{repo}/issues")
public void create(
    @PathVariable("owner") String owner,
    @PathVariable("repo") String repo,
    @RequestBody RequestBodyBean object) {
    System.out.println(object);

    object.getTitle();
    object.getLabels().get(0);
}

另一種 POST 請求體中的參數(shù)還可以是 form 表單的形式甫煞,這和查詢字符串其實(shí)是一樣的,只不過是放在了 body 中冠绢,所以還是繼續(xù)使用 @RequestParam 來處理這種形式的字符串參數(shù)抚吠。
一般適用于參數(shù)比較少的時(shí)候,也就不再單獨(dú)綁定一個(gè) Java Bean唐全,而是直接將參數(shù)取出:

@PostMapping("/login")
public void formDemo(
    @RequestParam("username") String username,
    @RequestParam("password") String password
) {
    System.out.println(username);
    System.out.println(password);
}
image.png
image.png

Postman 抓包看一下原始 http 請求文本可以驗(yàn)證埃跷,雖然使用了 POST,參數(shù)放在了請求體邮利,但因?yàn)槭褂?x-www-form-urlencoded弥雹,所以還是和查詢字符串的拼接形式一樣,還是拼出了字符串延届。其實(shí)很好理解剪勿,因?yàn)樾畔⒔涣鳎@樣子拼是雅信達(dá)的方庭。典型的比如登錄場景中厕吉,比較適合。至少用戶名密碼不會被記錄在 URL 地址中械念。

6. 返回 HTTP 響應(yīng)

  • 直接操作 HttpServletResponse 對象【簡單头朱、粗暴、原始】
  • 直接返回 HTML 字符串【簡單龄减、粗暴项钮、原始】
  • 返回對象,并自動序列化為 JSON 字符串【常用,@ResponseBody】
  • 模板引擎渲染【JSP/Velocity/Freemaker(參見下一篇筆記)】

Spring boot 底層內(nèi)嵌了 Servlet 容器烁巫,Servlet 是 Java 世界中 Web 容器的標(biāo)準(zhǔn)署隘。
從機(jī)器端口中讀取字節(jié)流,封裝成 Java 對象亚隙,方便上層 WebApp 處理磁餐,處理完之后再把返回值交給 Servlet 容器轉(zhuǎn)化為字節(jié)流作為 HTTP 的響應(yīng)。
字節(jié)流 <--> Servlet(HttpServletRequest/HttpServletResponse) <--> Java 對象

  • 操作“裸”的 Servlet 接口:
@RequestMapping("/servlet")
public void search(HttpServletRequest request, HttpServletResponse reponse) throws IOException {
    reponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
    reponse.getWriter().write("404 Not Found");
}
  • 使用 @ResponseBody 標(biāo)記響應(yīng)阿弃,默認(rèn)會被自動轉(zhuǎn)換為 JSON:
@RequestMapping("/demo")
@ResponseBody
public Object search() {
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Tony");
    map.put("friends", Arrays.asList("Tom", "Bob", "Eric"));
    return map;
}

Accpet/Content-Type 是一對 HTTP Header诊霹,請求方可以指定接受的媒體類型。

7. 周邊生態(tài)系統(tǒng)

  • HTTPS
  • 分布式部署
  • 擴(kuò)展功能
    • 數(shù)據(jù)庫
    • Redis緩存
    • 消息隊(duì)列
    • RPC(Dubbo/Spring Cloud)
    • 微服務(wù)化
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渣淳,一起剝皮案震驚了整個(gè)濱河市畅哑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌水由,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赛蔫,死亡現(xiàn)場離奇詭異砂客,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)呵恢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門鞠值,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人渗钉,你說我怎么就攤上這事彤恶。” “怎么了鳄橘?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵声离,是天一觀的道長。 經(jīng)常有香客問我瘫怜,道長术徊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任鲸湃,我火速辦了婚禮赠涮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暗挑。我一直安慰自己笋除,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布炸裆。 她就那樣靜靜地躺著垃它,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嗤瞎,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天墙歪,我揣著相機(jī)與錄音,去河邊找鬼贝奇。 笑死虹菲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掉瞳。 我是一名探鬼主播毕源,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陕习!你這毒婦竟也來了霎褐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤该镣,失蹤者是張志新(化名)和其女友劉穎冻璃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體损合,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡省艳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫁审。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跋炕。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖律适,靈堂內(nèi)的尸體忽然破棺而出辐烂,到底是詐尸還是另有隱情,我是刑警寧澤捂贿,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布纠修,位于F島的核電站,受9級特大地震影響眷蜓,放射性物質(zhì)發(fā)生泄漏分瘾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一吁系、第九天 我趴在偏房一處隱蔽的房頂上張望德召。 院中可真熱鬧,春花似錦汽纤、人聲如沸上岗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肴掷。三九已至敬锐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呆瞻,已是汗流浹背台夺。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痴脾,地道東北人颤介。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像赞赖,于是被迫代替她去往敵國和親滚朵。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348