天天看點

SpringBoot 詳解Spring Boot

Spring Boot

0. 概述

簡介

Spring Boot 基于 Spring 開發,本身并不提供 Spring 架構的核心特性以及擴充功能,隻是用于快速、靈活地開發新一代基于 Spring 架構的應用程式。也就是說,它并不是用來替代 Spring 的解決方案,而是和 Spring 架構緊密結合用于提升 Spring 開發者體驗的工具。Spring Boot 以約定大于配置的核心思想,預設幫我們進行了很多設定,多數 Spring Boot 應用隻需要很少的 Spring 配置。同時它內建了大量常用的第三方庫配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 應用中這些第三方庫幾乎可以零配置的開箱即用。

優點

  • 為所有Spring開發者更快的入門
  • 開箱即用,提供各種預設配置來簡化項目配置
  • 内嵌式容器簡化Web項目
  • 沒有備援代碼生成和XML配置的要求

準備

開發環境:

  • java 1.8
  • Maven 3.6.3
  • SpringBoot 2.5.0

開發工具:

  • IDEA

1. 第一個 Spring Boot 程式

1.1 建立項目

方法:使用 IDEA 直接建立項目

1、建立一個新項目

2、選擇 spring initializr , 其預設會去官網的快速建構工具中實作

3、填寫項目資訊(包名推薦隻保留 com.xxx 即可)

4、選擇初始化的元件(勾選 Spring Web 即可)

5、填寫項目路徑

6、等待項目建構成功

1.2 項目結構分析

  1. 程式的主啟動類 Application.java
  2. 一個核心配置檔案 application.properties
  3. 一個測試類 ApplicationTests.java
  4. 一個 pom.xml

1.3 編寫 HTTP 接口

步驟如下:

  1. 在主程式的同級目錄下,建立一個 controller 包
  2. 在包中建立一個 Controller 類

    HelloController.java:

    package com.wmwx.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/hello")
    public class HelloController {
        @RequestMapping("/hello")
        public String hello(){
            return "Hello, world!";
        }
    }
               
  3. 從主程式入口啟動程式,并在浏覽器位址欄中輸入 localhost:8080/hello
    頁面顯示:Hello, World!
               

1.4 pom 檔案分析

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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wmwx</groupId>
    <artifactId>springboot-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-01</name>
    <description>Study Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- spring-boot-starter是SpringBoot依賴的字首 -->
        <!-- web場景啟動器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springboot單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 打jar包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- 跳過項目運作測試用例 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
           

1.5 修改配置檔案

application.properties:

# 更改項目端口号
server.port=8081
           

1.6 修改 Banner 圖案

字元圖案線上生成網站:字元圖案線上生成

得到字元圖案後,到項目下的 resources 目錄下建立一個banner.txt 即可。

2. 自動裝配原理

2.1 pom.xml

父依賴 spring-boot-dependencies

  • 管理 Spring Boot 應用裡面所有依賴版本的地方,是 Spring Boot 的版本控制中心
  • 以後導入依賴預設不需要再寫版本。但如果導入的包沒有在依賴中管理,則仍需我們手動配置版本

啟動器 spring-boot-starter

  • Spring Boot 的啟動場景,會自動導入環境中所有的依賴
  • Spring Boot 會将所有的功能場景變成一個個啟動器
  • 我們想要使用什麼功能,隻需找到對應的啟動器即可

2.2 主程式

2.2.1 注解

@SpringBootApplication:标注這個類是一個 Spring Boot 的應用

  • @SpringBootConfiguration:标注這個類是一個 Spring Boot 配置類
    • @Configuration:标注這個類是一個 Spring 配置類
      • @Component:标注這個類是一個 Spring 元件
  • @EnableAutoConfiguration:自動配置
    • @AutoConfigurationPackage:自動配置包
      • @Import({Registrar.class}):導入包注冊
    • @Import({AutoConfigurationImportSelector.class}):導入自動配置選擇器
  • @ComponentScan:掃描與目前其同類同級的包

結論:Spring Boot 所有的配置都是在啟動的時候掃描并加載至 spring.factories 裡,當配置對應啟動器時就會自動生效

總結:

  1. SpringBoot 在啟動的時候從類路徑下的 META-INF/spring.factories 中擷取 EnableAutoConfiguration 指定的值
  2. 将這些值作為自動配置類導入容器,自動配置類就生效,幫我們進行自動配置工作
  3. 整個 J2EE 的整體解決方案和自動配置都在 springboot-autoconfigure 的 jar 包中
  4. 它會給容器中導入非常多的自動配置類 (xxxAutoConfiguration), 就是給容器中導入這個場景需要的所有元件,并配置好這些元件
  5. 有了自動配置類,免去了我們手動編寫配置注入功能元件等的工作

2.2.2 SpringApplication

這個類通過 run 方法開啟了一個服務,具體執行的動作如下:

1、推斷應用的類型是普通的項目還是Web項目

2、查找并加載所有可用初始化器 , 設定到initializers屬性中

3、找出所有的應用程式監聽器,設定到listeners屬性中

4、推斷并設定main方法的定義類,找到運作的主類

3. YAML 配置注入

3.1 YAML 概述

YAML 是 “YAML Ain’t a Markup Language”(YAML 不是一種标記語言)的遞歸縮寫。在開發的這種語言時,YAML 的意思其實是 “Yet Another Markup Language”(仍是一種标記語言)。

YAML 的文法和其他進階語言類似,并且可以簡單表達清單、散清單,标量等資料形态。它使用空白符号縮進和大量依賴外觀的特色,特别适合用來表達或編輯資料結構、各種配置檔案、傾印調試内容、檔案大綱(例如:許多電子郵件标題格式和YAML非常接近)。

YAML 的配置檔案字尾為 .yml,如 Spring Boot 的核心配置檔案:application.yml 。

使用 YAML 可以代替原本 .properties 格式的配置檔案。例如,修改端口号的寫法, 可以參照以下格式:

.properties:

# 更改項目端口号
server.port=8081
           

.yml:

# 更改項目端口号
server:
  port: 8081
           

3.2 基礎文法

YAML 對文法的要求十分嚴格:

  • 大小寫敏感
  • 使用縮進表示層級關系
  • 縮進不允許使用tab,隻允許空格
  • 縮進的空格數不重要,隻要相同層級的元素左對齊即可
  • '#'表示注釋

3.2.1 純量

純量是最基本的,不可再分的值,包括:

  • 字元串
  • 布爾值
  • 整數
  • 浮點數
  • Null
  • 時間
  • 日期

其文法如下:

key: value
           

即:純量直接寫在冒号的後面即可,中間使用空格隔開。另外, 字元串預設不用加上雙引号或者單引号,日期必須使用 yyyy/MM/dd 的格式。

3.2.2 對象

其文法如下:

#對象
name: 
    key1:value1
    key2:value2
           

或寫在行内則為:

#對象(行内寫法)
name: {key1: value1,key2: value2}
           

3.2.3 數組

其文法如下:

key:
 - value1
 - value2
 - value3
           

或寫在行内則為:

3.3 注入配置檔案

YAML 檔案強大的地方在于,它可以為實體類直接注入比對值。

之前我們為實體類指派時,可以采用 @Value() 注解來實作。示例:

  1. 編寫實體類

    Dog.java:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    public class Dog {
        @Value("小黑")
        private String name;
        @Value("3")
        private int age;
    }
               
  2. 編寫測試類
    package com.wmwx;
    
    import com.wmwx.pojo.Dog;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Dog dog;
    
        @Test
        void contextLoads() {
            System.out.println(dog);
        }
    
    }
               
  3. 輸出結果:
    Dog(name=小黑, age=3)
               

但是,在使用 YAML 之後,為實體類的屬性指派就多了一種方案:為實體類添加 @ConfigurationProperties(prefix = “”) 注解,并在 .yml 檔案中編寫對應的對象即可。

示例:

  1. 編寫實體類:

    Person.java:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private int age;
        private Boolean happy;
        private Date birthday;
        private Map<String, Object> number;
        private List<Object> hobbies;
        private Dog dog;
    }
               
  2. 編寫 YAML 配置檔案

    application.yml:

    # Person 對象屬性指派
    person:
      name: 劉季恒
      age: 18
      happy: true
      birthday: 1999/07/07
      number:
        # 使用占位符生成随機整數
        phone: ${random.int}
        # 使用占位符生成随機UID
        id: ${random.uuid}
      hobbies:
        - 街機遊戲
        - 古風歌曲
      dog:
        name: 小黑
        age: 3
               
  3. 添加依賴:
    <!-- 導入配置檔案處理器,配置檔案進行綁定就會有提示,需要重新開機 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
               
  4. 編寫測試類:
    package com.wmwx;
    
    import com.wmwx.pojo.Dog;
    import com.wmwx.pojo.Person;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Dog dog;
        @Autowired
        private Person person;
    
        @Test
        void contextLoads() {
            System.out.println(person);
        }
    
    }
               
  5. 輸出結果:
    Person(name=劉季恒, age=18, happy=true, birthday=Wed Jul 07 00:00:00 CST 1999, number={phone=1012029378, id=25d37c9a-4046-4185-9f8f-bbf6282dbf0d}, hobbies=[街機遊戲, 古風歌曲], dog=Dog(name=小黑, age=3))
               

@ConfigurationProperties 對比 @Value:

  1. @ConfigurationProperties 隻需寫一次,@Value 需要對每個字段逐一添加。
  2. @ConfigurationProperties 支援松散綁定,即:first-name 等同于 firstName。
  3. @ConfigurationProperties 支援 JSR303 資料校驗。
  4. @ConfigurationProperties 可以封裝對象,而 @Value 不可以。

3.4 JSR303 資料校驗

Springboot 中可以用 @validated 注解來校驗資料。如果資料異常則會統一抛出異常,友善異常中心統一處理。

首先,我們為

Person

類添加一個

email

屬性,以便于接下來的測試:

Person.java:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private int age;
    //電子郵件,我們希望隻接收電子郵箱格式的字元串
    private String email;
    private Boolean happy;
    private Date birthday;
    private Map<String, Object> number;
    private List<Object> hobbies;
    private Dog dog;
}
           

接着,我們為

Person

類添加 @validated 注解,并在

email

屬性的上方添加 @Email 注解:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "person")
@Validated      //資料校驗
public class Person {
    private String name;
    private int age;
	@Email(message="電子郵箱格式錯誤!")
    private String email;
    private Boolean happy;
    private Date birthday;
    private Map<String, Object> number;
    private List<Object> hobbies;
    private Dog dog;
}
           

這樣一來,當

email

屬性接收到錯誤格式的字元串時,就會報出 “電子郵箱格式錯誤!” 的資訊。

使用資料校驗,可以保證資料的正确性。記住,不要相信任何前端傳來的資料!

常用注解參數表:

//空檢查
@Null       //驗證對象是否為null
@NotNull    //驗證對象是否不為null, 無法查檢長度為0的字元串
@NotBlank   //檢查限制字元串是不是Null還有被Trim的長度是否大于0,隻對字元串,且會去掉前後空格.
@NotEmpty   //檢查限制元素是否為NULL或者是EMPTY.
    
//Booelan檢查
@AssertTrue     //驗證 Boolean 對象是否為 true  
@AssertFalse    //驗證 Boolean 對象是否為 false  
    
//長度檢查
@Size(min=, max=) 	//驗證對象(Array,Collection,Map,String)長度是否在給定的範圍之内  
@Length(min=, max=) //string is between min and max included.

//日期檢查
@Past       //驗證 Date 和 Calendar 對象是否在目前時間之前  
@Future     //驗證 Date 和 Calendar 對象是否在目前時間之後
    
//萬能檢查
@Pattern    //驗證 String 對象是否符合正規表達式的規則
           

4. 多配置檔案切換

4.1 配置檔案加載位置

外部加載配置檔案的方式十分多,springboot 啟動會掃描以下位置的 application.properties 或者 application.yml 檔案作為 Spring boot 的預設配置檔案:

優先級1:項目路徑下config目錄中的配置檔案
優先級2:項目路徑根目錄中的配置檔案
優先級3:資源路徑下config目錄中的配置檔案
優先級4:資源路徑根目錄的配置檔案
           

優先級由高到底,高優先級的配置會覆寫低優先級的配置。然後 SpringBoot 會從這四個位置全部加載主配置檔案,并進行互補配置。

4.2 多配置檔案

當我們編寫主配置檔案的時候,檔案名可以采用

application-{profile}.properties/yml

的格式 , 用于指定多個環境版本。

示例:

application-test.properties/yml:代表測試環境配置

application-dev.properties/yml:代表開發環境配置

但是,Springboot 并不會直接啟動這些配置檔案,它預設使用 application.properties/yml 作為主配置檔案。

我們可以通過一個配置來選擇需要激活的環境:

.properties:

# 更改配置檔案
spring.profiles.active = dev
           

.yml:

# 更改配置檔案
spring:
  profiles:
    active: dev
           

5. 深入了解自動裝配原理

配置檔案到底寫的是什麼?應該怎麼寫?

5.1 分析自動裝配原理

以 HttpEncodingAutoConfiguration(Http編碼自動配置) 為例分析自動配置原理:

//表示這是一個配置類,和以前編寫的配置檔案一樣,也可以給容器中添加元件;
@Configuration 

//啟動指定類的ConfigurationProperties功能;
  //進入這個HttpProperties檢視,将配置檔案中對應的值和HttpProperties綁定起來;
  //并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class}) 

//Spring底層@Conditional注解
  //根據不同的條件判斷,如果滿足指定的條件,整個配置類裡面的配置就會生效;
  //這裡的意思就是判斷目前應用是否是web應用,如果是,目前配置類生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

//判斷目前項目有沒有這個類CharacterEncodingFilter;SpringMVC中進行亂碼解決的過濾器;
@ConditionalOnClass({CharacterEncodingFilter.class})

//判斷配置檔案中是否存在某個配置:spring.http.encoding.enabled;
  //如果不存在,判斷也是成立的
  //即使我們配置檔案中不配置pring.http.encoding.enabled=true,也是預設生效的;
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)

public class HttpEncodingAutoConfiguration {
    //它已經和SpringBoot的配置檔案映射了
    private final Encoding properties;
    //隻有一個有參構造器的情況下,參數的值就會從容器中拿
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }
    
    //給容器中添加一個元件,這個元件的某些值需要從properties中擷取
    @Bean
    @ConditionalOnMissingBean //判斷容器沒有這個元件?
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
}
           

一句話總結 :根據目前不同的條件判斷,決定這個配置類是否生效!

  • 一旦這個配置類生效,就會給容器中添加各種元件
  • 這些元件的屬性是從對應的 Properties 類中擷取的,這些類裡面的每一個屬性又是和配置檔案綁定的
  • 所有在配置檔案中能配置的屬性都是在 Properties 類中封裝着
  • 配置檔案能配置什麼就可以參照某個功能對應的這個屬性類
//從配置檔案中擷取指定的值和bean的屬性進行綁定
@ConfigurationProperties(prefix = "spring.http") 
public class HttpProperties {
    // .....
}
           

例如,如果我們在配置檔案中輸入

spring.http.encoding.

就會看到 IDEA 的代碼補全中,顯示的正是 bean 中的屬性。這便是自動裝配的原理。

5.2 自動裝配的精髓

  1. SpringBoot 啟動會加載大量的自動配置類
  2. 首先看我們需要的功能有沒有在 SpringBoot 預設寫好的自動配置類當中
  3. 再來看這個自動配置類中到底配置了哪些元件(隻要我們要用的元件存在在其中,我們就不需要再手動配置了)
  4. 給容器中自動配置類添加元件的時候,會從 Properties 類中擷取某些屬性,我們隻需要在配置檔案中指定這些屬性的值即可

核心:

xxxxAutoConfigurartion:自動配置類,給容器中添加元件

xxxxProperties:封裝配置檔案中相關屬性

5.3 @Conditional

了解完自動裝配的原理後,我們來關注一個細節問題,即:自動配置類必須在一定的條件下才能生效。

而 @Conditional 注解 的作用就是:

  • 必須當 @Conditional 指定的條件成立,才會給容器中添加元件,配置配裡面的所有内容才生效。

那麼多的自動配置類,必須在一定的條件下才能生效。也就是說,我們加載了這麼多的配置類,但不是所有的都生效了。

我們怎麼知道哪些自動配置類生效?

我們可以在配置檔案中啟用 debug=true 屬性,讓控制台列印自動配置報告,這樣我們就可以很友善的知道哪些自動配置類生效。

application.properties:

#開啟springboot的調試類
debug=true
           

其中,可以看到,配置類型的生效情況共有以下三種:

  • Positive matches:自動配置類啟用的,正比對
  • Negative matches:沒有啟動,沒有比對成功的自動配置類:負比對
  • Unconditional classes:沒有條件的類

6. Web 開發前的準備

6.1 Spring Boot 的使用步驟

  1. 建立一個 SpringBoot 應用
  2. 選擇需要的子產品,SpringBoot 就會預設将我們的需要的子產品自動配置好
  3. 部分沒有被自動裝配的子產品需要手動裝配一下
  4. 專注編寫業務代碼即可,不需要再像以前那樣考慮一大堆的配置了

6.2 加載靜态資源

首先,搭建一個普通的 Spring Boot 項目。

在 SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 這個配置類裡面。可以看到,WebMvcAutoConfigurationAdapter 中有很多配置方法。其中有一個方法叫 addResourceHandlers,正是用來添加資源的處理類:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用預設資源處理
        logger.debug("Default resource handling disabled");
        return;
    }
    // 緩存控制
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    // webjars 配置
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 靜态資源配置
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}
           

可以發現,該源碼中提供了兩種資源配置的方式:**webjars 配置 **和 靜态資源映射。

6.2.1 webjars 配置

webjars 的本質就是以 jar 包的方式引入靜态資源 ,是以,隻需在 pom.xml 中引入對應的依賴,即可導入需要的靜态資源。

webjars 官網:webjars 官網

例如,當我們想要使用 jQuery 時,隻需在 pom.xml 中引入 jQuery 對應版本的依賴即可:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>
           

通路方式也很簡單:

http://localhost:8080/webjars/jquery/3.4.1/jquery.js

6.2.2 靜态資源映射

webjars 使用起來雖然很友善,但是卻有一個重要的問題:不能導入我們自己的靜态檔案!

是以,我們需要去找 staticPathPattern 的第二種方式:通過

/**

路徑通路目前的項目任意資源。

這個方法會去找 resourceProperties 這個類,我們可以看一下它的源碼:

// 進入方法
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 找到對應的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路徑
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
  	"classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};
           

ResourceProperties 可以設定與靜态資源有關的參數,在這裡它指向了會去尋找資源的檔案夾,即

CLASSPATH_RESOURCE_LOCATIONS

數組的内容。

得出結論,按照優先級從高到低的方式,以下四個目錄存放的靜态資源可以被我們識别:

  • classpath : /META-INF/resources/
  • classpath : /resources/
  • classpath : /static/
  • classpath : /public/

這樣一來,我們隻需在

resources

根目錄下建立對應名稱的檔案夾,就可以存放我們自己的靜态資源檔案了。

另外,如果我們在 application.properties 中自己制定了資源路徑檔案夾,那麼原本的自動配置就會失效。這是因為,源碼中是這樣寫的:

if (!this.resourceProperties.isAddMappings()){
    Logger.debug("Default resource handling disabled");
    return;
    //這裡傳回之後,後續代碼中的 webjars 和 資源映射 兩種方式都不會再去執行了
}
           

6.2.3 首頁

源碼中還有一段關于歡迎頁,也就是首頁的映射:

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService,
                                                           ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}
           

其中,

getWelcomePage()

方法就是擷取首頁的方法,其源碼如下:

private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    // ::是java8 中新引入的運算符
    // Class::function的時候function是屬于Class的,應該是靜态方法。
    // this::function的funtion是屬于這個對象的。
    // 簡而言之,就是一種文法糖而已,是一種簡寫
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 歡迎頁就是一個location下的的 index.html 而已
private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}
           

這就意味着,隻需在靜态資源映射檔案夾中建立一個名為

index.html

的檔案,它就會自動被映射為首頁,通過

localhost:8080

即可進行通路。

6.3 Thymeleaf 模闆引擎

6.3.1 模闆引擎

模闆引擎的作用就是将背景封裝的資料按照表達式解析并填充到頁面中指定的位置。之前使用的 jsp 其實就是一個模闆引擎。而在 SpringBoot 中,它給我們推薦的是 Thymeleaf 模闆引擎。它是一個進階語言的模闆引擎,它的文法更簡單、功能更強大。

6.3.2 引入 Thymeleaf

  • Thymeleaf 官網:Thymeleaf - 官網
  • Thymeleaf 在Github 的首頁:Thymeleaf - Github

想要引入 Thymeleaf,隻需在 pom.xml 檔案中導入相應的啟動器即可:

<!-- Thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
           

6.3.3 使用 Thymeleaf

首先去尋找 Thymeleaf 的自動配置類 ThymeleafProperties:

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
}
           

可以看出,使用 Thymeleaf 的預設字首是

classpath:/templates/

,而預設的字尾是

.html

這就意味着,我們隻需要把 html 頁面放在類路徑下的

templates

目錄下,thymeleaf 就可以幫我們自動渲染了。

測試:

  1. 編寫 ThymeleafController.java:
    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class ThymeleafController {
        @RequestMapping("/test")
        public String test(Model model){
            model.addAttribute("msg", "Thymeleaf 測試");
            return "test";
        }
    }
               
  2. 編寫 test.html,并将之放入

    templates

    目錄下:
    <!DOCTYPE html>
    <!-- 引入命名空間 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Thymeleaf 測試</title>
    </head>
    <body>
        <!--th:text就是将div中的内容設定為它指定的值 -->
        <div th:text="${msg}"></div>
    </body>
    </html>
               
  3. 在浏覽器位址欄中輸入

    localhost:8080/test

    ,進入頁面可以看到如下輸出結果:
    Thymeleaf 測試
               

6.4 MVC 自動配置原理

在進行實戰項目編寫前,我們還需要知道一個東西,那就是 SpringBoot 對 SpringMVC 還做了哪些配置,包括如何擴充、如何定制。隻有把這些都搞清楚了,之後使用起來才會更加得心應手。

想要搞清楚這些,途徑有二:

  1. 源碼分析
  2. 官方文檔:Spring Boot 官方文檔 - Spring Web MVC 架構

查閱可知,Spring Boot 在自動配置很多元件的時候,會先去檢視容器中有沒有使用者自己配置的配置類。如果有,就選擇使用者配置的;如果沒有,就啟用原本的自動配置。如果有些元件可以存在多個,那麼 Spring Boot 就會将使用者的配置與預設的配置組合起來進行裝配。

6.5 擴充 Spring MVC

官方文檔中有這麼一段描述:

If you want to take complete control of Spring MVC, you can add your own

@Configuration

annotated with

@EnableWebMvc

, or alternatively add your own

@Configuration

-annotated

DelegatingWebMvcConfiguration

as described in the Javadoc of

@EnableWebMvc

.

經過解讀,我們需要進行如下操作:

  1. 建立

    config

    目錄
  2. config

    目錄下建立一個類
  3. 令此類實作

    WebMvcConfigurer

  4. 為此類添加

    @Configuration

    注解
  5. 不能為此類添加

    @EnableWebMvc

    注解,否則自動裝配會全部失效

7. 整合 JDBC

7.1 Spring Data

對于資料通路層,無論是 SQL(關系型資料庫)還是 NOSQL(非關系型資料庫),Spring Boot 底層都是采用 Spring Data 的方式進行統一處理,它也是 Spring 中與 Spring Boot、Spring Cloud 等齊名的知名項目。

Sping Data 官網:Spring Data 官網

資料庫相關的啟動器 :可以參考官方文檔:官方文檔

7.2 搭建整合 JDBC 的項目

  1. 建立一個項目,在建立時選擇引入以下子產品:
    • Spring Web
    • JDBC API
    • MySQL Driver
  2. 項目建立完成後,可以在 pom.xml 中看到 Spring Boot 已經自動導入了如下的啟動器:
    <!-- JDBC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <!-- WEB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MySQL -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
               
  3. 在 application.yaml 中編寫資料庫配置
    spring:
      datasource:
        # 資料庫使用者名&密碼:
        username: root
        password: 123456
        # 資料庫連接配接位址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 資料庫驅動:
        driver-class-name: com.mysql.cj.jdbc.Driver
               
  4. 由于 SpringBoot 已經預設幫我們進行了自動配置,是以我們直接去使用就可以了。在測試類裡面測試一下:
    package com.wmwx;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import javax.sql.DataSource;
    
    @SpringBootTest
    class Springboot03ApplicationTests {
    
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            //檢視預設資料源
            System.out.println(dataSource.getClass());
        }
    
    }
               
  5. 輸出結果:
    class com.zaxxer.hikari.HikariDataSource
               

這就意味着,在我們沒有手動配置的情況下,Spring Boot 的預設資料源為

class com.zaxxer.hikari.HikariDataSource

。HikariDataSource 号稱是 Java WEB 目前速度最快的資料源,相比于傳統的 C3P0 、DBCP、Tomcat jdbc 等連接配接池更加優秀。

如果我們想要自定義資料源,可以使用 spring.datasource.type 指定要使用的連接配接池實作的完全限定名。

7.3 JDBC Template

有了資料源之後,我們就可以拿到資料庫連接配接:

@SpringBootTest
class Springboot03ApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //擷取連接配接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();
    }

}
           

而有了連接配接,即使不使用第三方第資料庫操作架構,如 MyBatis 等,Spring 本身也對原生的 JDBC 做了輕量級的封裝,我們也可以使用原生的 JDBC 語句來操作資料庫。這個封裝便是 Jdbc Template,資料庫操作的所有 CRUD 方法都在其中。并且,Spring Boot 不僅提供了預設的資料源,同時預設配置好了 Jdbc Template 放在了容器中。這就意味着,程式員隻需自己注入,便可直接使用。

JdbcTemplate主要提供以下幾類方法:

  • execute 方法:可以用于執行任何 SQL 語句,一般用于執行 DDL 語句
  • update 方法及 batchUpdate 方法:update 方法用于執行新增、修改、删除等語句;batchUpdate 方法用于執行批處理相關語句
  • query 方法及 queryForXXX 方法:用于執行查詢相關語句;
  • call 方法:用于執行存儲過程、函數相關語句。

測試步驟:

  1. 編寫一個 Controller,并注入 JDBC Template:

    JDBCController.java:

    package com.wmwx.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/jdbc")
    public class JDBCController {
        @Autowired
        JdbcTemplate jdbcTemplate;
    
        //查詢資料庫的所有資訊
        //沒有實體類時,資料庫中的資料可以通過Map來擷取
        @GetMapping("/userList")
        public List<Map<String, Object>> getUserList(){
            String sql = "select * from user";
            return jdbcTemplate.queryForList(sql);
        }
    }
               
  2. 在浏覽器位址欄中輸入

    localhost:8080/jdbc/userList

    ,可以看到頁面中的輸出如下:
    [{"id":1,"name":"劉季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉靈","pwd":"101010"},{"id":4,"name":"鄧淩光","pwd":"197812"},{"id":5,"name":"周龍","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"}]
               
  3. 為 Controller 添加增加使用者的功能:

    JDBCController.java:

    //增加
    @RequestMapping("/addUser")
    public String addUser(){
        String sql = "insert into user values(7, '李章泯', '010101')";
        jdbcTemplate.update(sql);
        return "addUser-success!";
    }
               
  4. 在浏覽器位址欄中輸入

    localhost:8080/jdbc/addUser

    ,可以看到頁面中的輸出如下:
    addUser-success!
               
  5. 在浏覽器位址欄中輸入

    localhost:8080/jdbc/userList

    ,可以看到頁面中的輸出如下:
    [{"id":1,"name":"劉季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉靈","pwd":"101010"},{"id":4,"name":"鄧淩光","pwd":"197812"},{"id":5,"name":"周龍","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"},{"id":7,"name":"李章泯","pwd":"010101"}]
               
    可見使用者的确已經添加成功。
  6. 同理,可為 Controller 添加修改使用者和删除使用者的功能:

    JDBCController.java:

    //修改
    @RequestMapping("/updateUser/{id}")
    public String updateUser(@PathVariable int id){
        String sql = "update user set name=?,pwd=? where id="+id;
        //封裝
        Object[] objects = new Object[2];
        objects[0] = "老金";
        objects[1] = "222222";
        jdbcTemplate.update(sql, objects);
        return "updateUser-success!";
    }
    
    //删除
    @RequestMapping("/deleteUser")
    public String deleteUser(){
        String sql = "delete from user where id=7";
        jdbcTemplate.update(sql);
        return "deleteUser-success!";
    }
               

8. 整合 Druid

8.1 Druid 簡介

Druid 是阿裡巴巴開源平台上一個資料庫連接配接池實作,結合了 C3P0、DBCP 等 DB 池的優點,同時加入了日志監控。已經在阿裡巴巴部署了超過600個應用,經過了一年多生産環境大規模部署的嚴苛考驗。它可以很好地監控 DB 池連接配接和 SQL 的執行情況。可以說,它天生就是針對監控而生的 DB 連接配接池。

Github位址:https://github.com/alibaba/druid/

8.2 使用 Druid

Spring Boot 2.0 以上預設使用 Hikari 資料源,我們需要在配置檔案中重新指定為 Druid。

使用步驟:

  1. 在 pom.xml 中添加 Druid 資料源依賴:
    <!-- Druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
               
  2. 在 application.yaml 中切換資料源:
    spring:
      datasource:
        # 資料庫使用者名&密碼:
        username: root
        password: 123456
        # 資料庫連接配接位址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 資料庫驅動:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切換資料源為Druid
        type: com.alibaba.druid.pool.DruidDataSource
               
  3. 為資料源配置參數:
    spring:
      datasource:
        # 資料庫使用者名&密碼:
        username: root
        password: 123456
        # 資料庫連接配接位址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 資料庫驅動:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切換資料源為Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己綁定
        #druid 資料源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
               
  4. 為資料源配置日志監控
    spring:
      datasource:
        # 資料庫使用者名&密碼:
        username: root
        password: 123456
        # 資料庫連接配接位址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 資料庫驅動:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切換資料源為Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己綁定
        #druid 資料源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #配置監控統計攔截的filters,stat:監控統計、log4j:日志記錄、wall:防禦sql注入
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
               
  5. 在 pom.xml 檔案中導入 log4j 的依賴
    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
               
  6. 啟動浏覽器,測試 7.3 中的增删改查,發現與之前使用 Hikari 時并無不同。
  7. 此時需要建立一個 Config 類,我們自己為 DruidDataSource 綁定全局配置檔案中的參數,然後添加到容器中:
    package com.wmwx.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Configuration
    public class DruidConfig {
        /*
           将自定義的 Druid資料源添加到容器中,不再讓 Spring Boot 自動建立
           綁定全局配置檔案中的 druid 資料源屬性到 com.alibaba.druid.pool.DruidDataSource 進而讓它們生效
        */
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    }
               

8.3 配置 Druid 資料源監控

Druid 資料源具有監控的功能,并提供了一個 web 界面友善使用者檢視。想要使用這個功能,需要在 8.2 的代碼基礎上,為 DruidConfig.java 進行簡單的配置:

//背景監控 相當于web.xml
//由于SpringBoot内置了Servlet,是以沒有web.xml
//是以當我們想要使用web.xml時,就需要注冊ServletRegistrationBean
@Bean
public ServletRegistrationBean statViewServlet(){
    ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    //背景需要有人登入,賬号密碼配置
    HashMap<String, String> initParameters = new HashMap<>();
    //增加配置
    //登入的使用者名和密碼的Key是固定的,即loginUsername和loginPassword,不可以更換
    initParameters.put("loginUsername", "admin");
    initParameters.put("loginPassword", "123456");
    //允許誰可以通路
    initParameters.put("allow",""); //value為空意味着所有人都能通路
    //設定初始化參數
    bean.setInitParameters(initParameters);
    return bean;
}
           

測試步驟:

  1. 打開浏覽器,在位址欄中輸入

    localhost:8080/druid

    ,進入 Druid 背景登入界面
  2. 使用者名輸入

    admin

    ,密碼輸入

    123456

    ,點選 “login” 按鈕即可登入 Druid 監控背景
  3. 浏覽器新标簽頁的位址欄中輸入

    localhost:8080/jdbc/userList

    ,并重新整理監控背景,即可在 SQL 監控中看到被執行的 SQL 語句

8.4 配置 Druid 過濾器

示例:

DruidConfig.java:

//filter 過濾器
@Bean
public FilterRegistrationBean webStatFilter(){
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());
    //配置可以過濾的請求
    HashMap<String, String> initParameters = new HashMap<>();
    //exclusions:不參與統計
    initParameters.put("exclusions", "*.js,*.css,/druid/*");
    bean.setInitParameters(initParameters);
    return bean;
}
           

9. 整合 MyBatis

9.1 簡介

想要在 Spring Boot 内整合 MyBatis,需要一個啟動器:mybatis-spring-boot-starter 的支援。與其他大多數啟動器不同,這個啟動器并非 Spring Boot 官方推出的,而是 MyBatis 官方推出的,作用就是為了整合 MyBatis 與 Spring Boot。也正是以,該啟動器的名稱以

mybatis

開頭,而非以

sping-boot-stater

開頭。

官方文檔:mybatis-spring-boot-starter 官方文檔

9.2 整合

  1. 在 pom.xml 中導入 MyBatis 所需要的依賴
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
               
  2. 在 pom.xml 中導入 lombok 依賴,便于實體類的建立:
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
               
  3. 建立

    pojo

    目錄,并在其中建立實體類 User.java:
    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
               
  4. 建立

    mapper

    目錄,并在其中建立接口 UserMapper.java:
    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    //@Mapper表明這是一個MyBatis的Mapper類
    @Mapper
    //@Repository是@Component的變形
    @Repository
    public interface UserMapper {
        //查詢所有的使用者
        public List<User> queryUserList();
    }
               
  5. static

    目錄下建立

    /mybatis/UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wmwx.mapper.UserMapper">
        <select id="queryUserList" resultType="User">
            select * from user;
        </select>
    </mapper>
               
  6. 在 application.yaml 中配置整合 MyBatis
    # 整合 Mybatis
    mybatis:
      # 别名
      type-aliases-package: com.wmwx.pojo
      # Mapper配置檔案的位置
      mapper-locations: classpath:/static/mybatis/*.xml
               
  7. controller

    目錄中建立一個 Controller 類:

    UserController.java:

    package com.wmwx.controller;
    
    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        private UserMapper userMapper;
    
        @RequestMapping("/userList")
        public List<User> queryUserList(){
            List<User> userList = userMapper.queryUserList();
            return userList;
        }
    }
               
  8. 打開浏覽器,在位址欄中輸入

    localhost:8080/user/userList

    ,可以發現頁面輸出如下:
    [{"id":1,"name":"劉季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉靈","pwd":"101010"},{"id":4,"name":"鄧淩光","pwd":"197812"},{"id":5,"name":"周龍","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"},{"id":7,"name":"老金","pwd":"222222"}]
               

10. Spring Security

10.1 安全問題

在實際的 Web 開發中,安全一直是非常重要的一個問題。它本身雖然屬于非功能性需求,但如果直到應用開發的後期才去考慮,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,可能造成使用者的隐私資料被攻擊者竊取;另一方面,應用的基本架構已經确定,想要修複安全漏洞,就有可能對系統的架構做出比較重大的調整,導緻拖延開發時間,影響應用的釋出程序。是以,從應用開發的第一天起,就應該把安全相關的因素考慮進來,并應用在整個開發過程中。

目前市面上比較主流的安全架構主要有兩個:

  1. Spring Security
  2. Shiro

10.2 Spring Security 簡介

Spring Security 是一個功能強大且高度可定制的身份驗證和通路控制架構。它實際上是保護基于 spring 的應用程式的标準,側重于為 Java 應用程式提供身份的驗證與授權。與所有 Spring 項目一樣,Spring Security 的真正強大之處在于它面向切面,可以輕松擴充以滿足定制需求。

Spring Security 是針對 Spring 項目的安全架構,也是 Spring Boot 底層安全子產品預設的技術選型。它可以實作強大的 Web 安全控制,我們僅需要引入

spring-boot-starter-security

子產品,進行少量的配置,即可實作強大的安全管理!

核心類:

  • WebSecurityConfigurerAdapter:自定義 Security 政策
  • AuthenticationManagerBuilder:自定義認證政策
  • @EnableWebSecurity:開啟 WebSecurity 模式

如前文所說,Spring Security 的兩個主要目标是 “認證” 和 “授權”(通路控制)。

“認證”(Authentication)

身份驗證是關于驗證您的憑據,如使用者名/使用者 ID 和密碼,以驗證您的身份。

身份驗證通常通過使用者名和密碼完成,有時與身份驗證因素結合使用。

“授權” (Authorization)

授權發生在系統成功驗證您的身份後,最終會授予您通路資源的完全權限。

這個概念是通用的,而不是隻在 Spring Security 中存在。

10.3 實戰測試

10.3.1 環境搭建

  1. 建立一個初始的 Spring Boot 項目,勾選 Web 子產品和 Thymeleaf 子產品
  2. templates

    目錄下建立

    views

    目錄,并在

    views

    目錄下建立

    level1

    level2

    level3

    目錄
  3. 按照以下結建構立靜态資源:
    • views
      • level1
        • 1.html
        • 2.html
        • 3.html
      • level2
        • 1.html
        • 2.html
        • 3.html
      • level3
        • 1.html
        • 2.html
        • 3.html
    • index.html
  4. 編寫靜态資源:

    index.html:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
    <div>
        <!-- 注意這裡的兩個路徑,不可以改變 -->
        <a th:href="@{/login}">登入</a>
        <a th:href="@{/logout}">登出</a>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level1/1}">level1-1</a></li>
            <li><a th:href="@{/level1/2}">level1-2</a></li>
            <li><a th:href="@{/level1/3}">level1-3</a></li>
        </ul>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level2/1}">level2-1</a></li>
            <li><a th:href="@{/level2/2}">level2-2</a></li>
            <li><a th:href="@{/level2/3}">level2-3</a></li>
        </ul>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level3/1}">level3-1</a></li>
            <li><a th:href="@{/level3/2}">level3-2</a></li>
            <li><a th:href="@{/level3/3}">level3-3</a></li>
        </ul>
    </div>
    </body>
    </html>
               
    levelm-n.html(m、n均為1~3):
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>levelm-n</title>
    </head>
    <body>
        <h1>levelm-n</h1>
    </body>
    </html>
               
  5. 建立

    controller

    目錄,并在其中建立

    RouterController

    類:
    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class RouterController {
        @RequestMapping({"/","/index"})
        public String toIndex(){
            return "index";
        }
    
        @RequestMapping("/level1/{id}")
        public String toLevel1(@PathVariable int id){
            return "views/level1/"+id;
        }
    
        @RequestMapping("/level2/{id}")
        public String toLevel2(@PathVariable int id){
            return "views/level2/"+id;
        }
    
        @RequestMapping("/level3/{id}")
        public String toLevel3(@PathVariable int id){
            return "views/level3/"+id;
        }
    }
               
  6. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,測試除登入與登出之外的各個連結之間的跳轉,均能成功。

10.3.2 認證和授權

  1. 在 pom.xml 中添加依賴
    <!-- security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
               
  2. 建立

    config

    目錄,在其中建立

    SecurityConfig.java

    ,并令其繼承

    WebSecurityConfigurerAdapter

    package com.wmwx.config;
    
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
        }
    }
               
  3. SecurityController.java

    中定制請求授權的規則:
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //授權
        //鍊式程式設計
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首頁所有人都可以通路,但是功能頁必須有權限的人才能通路
            //請求授權的規則
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
        }
    }
               
  4. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,測試各個連結之間的跳轉,可以看到所有的 level 頁面都會報 403 錯誤(權限不足)。
  5. SecurityController.java

    中開啟自動配置的登入功能:
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //授權
        //鍊式程式設計
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首頁所有人都可以通路,但是功能頁必須有權限的人才能通路
            //請求授權的規則
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
    
            //開啟自動配置的登入功能,若沒有權限預設回到登入頁
            http.formLogin();
        }
    }
               
  6. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,測試各個連結之間的跳轉,可以看到所有的 level 頁面都會進入登入頁。
  7. SecurityController.java

    中重新定義認證規則:
    //認證
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通常從資料庫中認證,此處先從記憶體中認證
        auth.inMemoryAuthentication()
                .withUser("root").password("123456").roles("vip1","vip2","vip3")
                .and()
                .withUser("wmwx").password("123456").roles("vip1","vip2")
                .and()
                .withUser("guest").password("123456").roles("vip1");
    }
               
  8. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,測試各個連結之間的跳轉,通過點選 level 頁進入登入頁,但在登入後卻報出 500 錯誤。
  9. 為密碼字元串加密:
    //認證
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通常從資料庫中認證,此處先從記憶體中認證
        //使用BCryptPasswordEncoder加密
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("wmwx").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }
               
  10. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,測試各個連結之間的跳轉,通過點選 level 頁進入登入頁,輸入使用者名與密碼,發現在具有相應權限的情況下會登入成功,而逾越了權限的情況下則會報出 403 錯誤。

10.3.3 登出

  1. 開啟自動配置的登出的功能
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首頁所有人都可以通路,但是功能頁必須有權限的人才能通路
        //請求授權的規則
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");
    
        //開啟自動配置的登入功能,沒有權限預設回到登入頁
        http.formLogin();
        //開啟自動配置的登出功能,登出後跳轉到首頁
        http.logout().logoutSuccessUrl("/");
    }
               
  2. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,登入後再點選登出,可以發現頁面自動跳轉到了首頁,且所有的權限都消失了。

10.3.4 權限控制

現在還有一個問題需要解決:使用者登入之後,不應該再看到不具備權限的内容,這樣就不會出現 403 報錯頁面了。

想要解決這個問題,需要借助 Thymeleaf 整合 Spring Security:

  1. 在 pom.xml 中導入整合包依賴:
    <!-- thymeleaf 整合 security -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
               
  2. 修改

    index.html

    中的登入與登出:
    <div>
        <!-- 登入前:顯示登入按鈕 -->
        <div sec:authorize="!isAuthenticated()">
            <a th:href="@{/login}">登入</a>
        </div>
        <!-- 登入後:顯示使用者名、權限和登出按鈕 -->
        <div sec:authorize="isAuthenticated()">
            使用者名:<span sec:authentication="name"></span>
            權限:<span sec:authentication="principal.authorities"></span>
        </div>
        <div sec:authorize="isAuthenticated()">
            <a th:href="@{/logout}">登出</a>
        </div>
    </div>
               
  3. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,隻能看到登入按鈕;登入後自動跳轉到首頁,可以看到使用者名與權限,測試成功。但是,此時我們再去點選登出按鈕,會發現報出了 404 錯誤。
  4. 出現這個問題是因為 Spring Security 預設防止 csrf 跨站請求僞造,因為它會産生安全問題。我們可以通過将請求改為 post 表單送出,或者在 Spring Security 中關閉 csrf 功能的方式來解決這個問題。這裡,我們選擇後者:
    //開啟自動配置的登出功能,登出後跳轉到首頁
    //關閉csrf功能:跨站請求僞造,預設隻能通過post方式送出logout請求
    http.csrf().disable();
    http.logout().logoutSuccessUrl("/");
               
  5. 同理可以修改

    index.html

    中的三個 level 塊:
    <div>
        <!-- 判斷使用者是否具有vip1的權限 -->
        <ul sec:authorize="hasRole('vip1')">
            <li><a th:href="@{/level1/1}">level1-1</a></li>
            <li><a th:href="@{/level1/2}">level1-2</a></li>
            <li><a th:href="@{/level1/3}">level1-3</a></li>
        </ul>
    </div>
    <div>
        <ul sec:authorize="hasRole('vip2')">
            <li><a th:href="@{/level2/1}">level2-1</a></li>
            <li><a th:href="@{/level2/2}">level2-2</a></li>
            <li><a th:href="@{/level2/3}">level2-3</a></li>
        </ul>
    </div>
    <div>
        <ul  sec:authorize="hasRole('vip3')">
            <li><a th:href="@{/level3/1}">level3-1</a></li>
            <li><a th:href="@{/level3/2}">level3-2</a></li>
            <li><a th:href="@{/level3/3}">level3-3</a></li>
        </ul>
    </div>
               
  6. 在浏覽器中位址欄輸入

    localhost:8080

    進入首頁,根據登入角色的不同,頁面中顯示出的内容也不同。

10.3.5 記住登入狀态

現在我們在登入後,每次重新打開浏覽器進入頁面,都需要重新登入,原本的登入狀态不會儲存。但是,很多網站在登入時都有一個記住登入狀态的功能,勾選後短期内就無需再次登入了。這個功能實作起來也很簡單,隻需一行代碼即可:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //首頁所有人都可以通路,但是功能頁必須有權限的人才能通路
    //請求授權的規則
    http.authorizeRequests().antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    //開啟自動配置的登入功能,沒有權限預設回到登入頁
    http.formLogin();
    //開啟自動配置的登出功能,登出後跳轉到首頁
    //關閉csrf功能:跨站請求僞造,預設隻能通過post方式送出logout請求
    http.csrf().disable();
    http.logout().logoutSuccessUrl("/");
    //開啟記住登入狀态功能,預設儲存兩周,本質是cookie
    http.rememberMe();
}
           

10.3.6 登入頁定制

到目前為止,我們使用的登入頁面都是 Spring Security 預設的。那麼,如何才能使用我們自己寫的登入頁面呢?

  1. views

    目錄下建立一個

    login.html

    檔案:
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登入</title>
    </head>
    <body>
        <form method="post" th:action="@{/login}">
            <div>使用者名:<input type="text" placeholder="請輸入使用者名" name="user"></div>
            <div>密碼:<input type="password" name="pwd"></div>
            <div><input type="checkbox" name="remember">記住我的登入狀态</div>
            <div><input type="submit" value="登入"></div>
        </form>
    </body>
    </html>
               
  2. 修改首頁中登入按鈕的路徑:
  3. 在 RouterController.java 中添加路由:
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }
               
  4. 在 SecurityConfig.java 中配置登入頁和記住登入狀态的功能:
    //定制登入頁
    http.formLogin()
            .loginPage("/toLogin")
            .usernameParameter("user")	//input标簽中使用者名的name屬性
            .passwordParameter("pwd")	//input标簽中密碼的name屬性
            .loginProcessingUrl("/login");
               
    //開啟記住登入狀态功能,預設儲存兩周,本質是cookie
    http.rememberMe().rememberMeParameter("remember");		//input标簽中記住登入狀态的name屬性
               
  5. 在浏覽器中測試,登入、登出,一切正常!

11. Shiro

11.1 Shiro 簡介

Apache Shiro 是一個 Java 的安全(權限)架構。它可以非常容易地開發出足夠好的應用,不僅可以用在 JavaSE 環境,也可以用在 JavaEE 環境。Shiro 可以實作認證、授權、加密、會話管理、Web內建、緩存等功能。

  • 下載下傳位址: Shrio 官網

核心:

  • Subject
  • Shiro Security Manager
  • Realm

11.2 Spring Boot 內建 Shiro

  1. 建立一個初始的 Spring Boot 項目,勾選 Web 子產品和 Thymeleaf 子產品
  2. 在 pom.xml 中導入依賴:
    <!-- shiro-spring -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.7.1</version>
    </dependency>
               
  3. 建立目錄

    config

    ,并在其中建立

    ShiroConfig.java

    package com.wmwx.config;
    
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ShiroConfig {
        //ShiroFilterFactoryBean
        
        //DefaultWebSecurityManager
    
        //建立Realm對象,需要自定義類
    
    }
               
  4. config

    目錄下建立

    UserRealm.java

    ,并令其繼承

    AuthorizingRealm

    package com.wmwx.config;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    //自定義的Realm
    public class UserRealm extends AuthorizingRealm {
        //授權
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("執行了授權方法");
            return null;
        }
        //認證
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("執行了認證方法");
            return null;
        }
    }
               
  5. 從下至上,補充

    ShiroConfig.java

    //建立Realm對象,需要自定義類
    @Bean(name="userRealm")
    public UserRealm userRealm(){
        return new UserRealm();
    }
               
  6. 繼續補充

    ShiroConfig.java

    //DefaultWebSecurityManager
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //關聯UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }
               
  7. 繼續補充

    ShiroConfig.java

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //設定安全管理器
        bean.setSecurityManager(securityManager);
        return bean;
    }
               
  8. templates

    目錄下建立一個

    user

    目錄,并在其中建立兩個

    .html

    頁面:

    add.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>新增使用者</title>
    </head>
    <body>
    <h1>新增使用者</h1>
    </body>
    </html>
               
    update.html:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>修改使用者</title>
    </head>
    <body>
    <h1>修改使用者</h1>
    </body>
    </html>
               
  9. templates

    目錄下建立一個

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
        <h1>首頁</h1>
        <a th:href="@{/user/add}">新增使用者</a>
        <a th:href="@{/user/update}">修改使用者</a>
    </body>
    </html>
               
  10. 建立一個

    controller

    目錄,并在其中建立一個

    ShiroController.java

    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class ShiroController {
        @RequestMapping({"/","/index"})
        public String toIndex(){
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String toAdd(){
            return "/user/add";
        }
    
        @RequestMapping("/user/update")
        public String toUpdate(){
            return "/user/update";
        }
    }
               
  11. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選連結進行跳轉測試,都可以成功進入。

11.3 Shiro 實作登入攔截

  1. ShiroConfig.java

    中添加 shiro 的内置過濾器:
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //設定安全管理器
        bean.setSecurityManager(securityManager);
        //添加shiro的内置過濾器
        /*
        * anon:無需認證即可通路
        * authc:必須認證才能通路
        * user:必須擁有記住我功能才能通路
        * perms:擁有對某個功能的權限才能通路
        * role:擁有某個角色的權限才能通路
        */
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add", "authc");
        filterMap.put("/user/update", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }
               
  2. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選連結進行跳轉測試,發現兩個連結點進去都報出 404 錯誤。
  3. templates

    目錄下建立一個

    login.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登入</title>
    </head>
    <body>
        <h1>登入</h1>
        <hr />
        <form method="post" th:action="@{/login}">
            <div>使用者名:<input type="text" name="username"></div>
            <div>密碼:<input type="password" name="password"></div>
            <div><input type="checkbox" name="remember">記住登入狀态</div>
            <div><input type="submit" value="登入"></div>
        </form>
    </body>
    </html>
               
  4. ShrioController.java

    中新增一個方法:
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
               
  5. ShiroConfig.java

    中設定登入請求:
    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //設定安全管理器
        bean.setSecurityManager(securityManager);
        //添加shiro的内置過濾器
        /*
        * anon:無需認證即可通路
        * authc:必須認證才能通路
        * user:必須擁有記住我功能才能通路
        * perms:擁有對某個功能的權限才能通路
        * role:擁有某個角色的權限才能通路
        */
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add", "authc");
        filterMap.put("/user/update", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
    
        //設定登入請求
        bean.setLoginUrl("/toLogin");
    
        return bean;
    }
               
  6. 修改

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
        <h1>首頁</h1>
        <a th:href="@{/toLogin}">登入</a>
        <hr />
        <a th:href="@{/user/add}">新增使用者</a>
        <a th:href="@{/user/update}">修改使用者</a>
    </body>
    </html>
               
  7. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選連結進行跳轉測試,發現點選三個連結都會自動跳轉到登陸頁面。

11.4 Shiro 實作使用者認證

  1. ShrioController.java

    中新增一個方法:
    @RequestMapping("/login")
    public String login(String username, String password, Model model){
        //擷取目前使用者
        Subject subject = SecurityUtils.getSubject();
        //封裝使用者的登入資料
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //執行登入的方法
        try {
            subject.login(token);
            return "index";
        }catch (UnknownAccountException e){
            model.addAttribute("msg", "使用者名不存在!");
            return "login";
        }catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密碼錯誤!");
            return "login";
        }
    }
               
  2. 修改

    login.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登入</title>
    </head>
    <body>
        <h1>登入</h1>
        <p th:text="${msg}" style="color: red;"></p>
        <hr />
        <form method="post" th:action="@{/login}">
            <div>使用者名:<input type="text" name="username"></div>
            <div>密碼:<input type="password" name="password"></div>
            <div><input type="checkbox" name="remember">記住登入狀态</div>
            <div><input type="submit" value="登入"></div>
        </form>
    </body>
    </html>
               
  3. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選任意連結進入登入頁,輸入任意字元後點選登入按鈕,發現頁面中用紅字輸出以下内容:
    使用者名不存在!
               
    而在控制台中則輸出以下内容:
    執行了認證方法
               
  4. 修改

    UserRealm.java

    //認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //使用者名、密碼可以從資料庫中擷取,此處先預先寫死
        String username = "root";
        String password = "123456";
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        if (!token.getUsername().equals(username)){
            return null;    //抛出UnknownAccountException異常,即使用者名不存在的異常
        }
        //IncorrectCredentialsException異常,即密碼錯誤的異常交給Shiro來檢測
        return new SimpleAuthenticationInfo("",password,"");
    }
               
  5. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選任意連結進入登入頁,故意輸錯使用者名後點選登入按鈕,可以看到紅字輸出:
    使用者名不存在!
               
    在使用者名框中輸入 “root”,在密碼框中故意輸錯,再次點選登入按鈕,可以看到紅字輸出:
    密碼錯誤!
               
    在使用者名框中輸入 “root”,并在密碼框中輸入 “123456”,點選登入按鈕,連結自動跳轉至首頁。此時,再次點選 “新增使用者” 或 “修改使用者”,都可以進入到對應的頁面中,而不會再次跳轉至登入頁面。

11.5 Shiro 整合 MyBatis

  1. 在 pom.xml 中導入依賴:
    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!-- Druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
               
  2. 編寫 application.yaml 配置檔案:
    spring:
      datasource:
        # 資料庫使用者名&密碼:
        username: root
        password: 123456
        # 資料庫連接配接位址
        url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 資料庫驅動:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切換資料源為Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己綁定
        #druid 資料源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #配置監控統計攔截的filters,stat:監控統計、log4j:日志記錄、wall:防禦sql注入
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    mybatis:
      # 為實體類起别名
      type-aliases-package: com.wmwx.pojo
      # 綁定Mapper配置檔案
      mapper-locations: classpath:mapper/*.xml
               
  3. 建立

    pojo

    目錄,并在此目錄下建立

    User.java

    檔案:
    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
               
  4. 建立

    mapper

    目錄,并在此目錄下建立

    UserMapper.java

    UserMapper.xml

    UserMapper.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    @Repository
    @Mapper
    public interface UserMapper {
        //通過使用者名查詢使用者
        public User queryUserByUsername(String name);
    }
               
    UserMapper.xml:
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wmwx.mapper.UserMapper">
        <!--
            //通過使用者名查詢使用者
            public User queryUserByUsername(String name);
        -->
        <select id="queryUserByUsername" resultType="User">
            select * from user where name=#{name}
        </select>
    </mapper>
               
  5. 建立

    service

    目錄,并在此目錄下建立

    UserService.java

    UserServiceImpl.java

    UserService.java:

    package com.wmwx.service;
    
    import com.wmwx.pojo.User;
    
    public interface UserService {
        //通過使用者名查詢使用者
        public User queryUserByUsername(String name);
    }
               
    UserServiceImpl.java:
    package com.wmwx.service;
    
    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserServiceImpl implements UserService{
        @Autowired
        UserMapper userMapper;
    
        @Override
        public User queryUserByUsername(String name) {
            return userMapper.queryUserByUsername(name);
        }
    }
               
  6. 編寫測試類:
    package com.wmwx;
    
    import com.wmwx.pojo.User;
    import com.wmwx.service.UserServiceImpl;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot05ApplicationTests {
        @Autowired
        UserServiceImpl userService;
    
        @Test
        void contextLoads() {
            User user = userService.queryUserByUsername("root");
            System.out.println(user);
        }
    
    }
               
  7. 運作

    contextLoads()

    方法,控制台中出現如下輸出:
    User(id=1, name=root, pwd=123456)
               
  8. 修改

    UserRealm.java

    //認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //使用者名、密碼從資料庫中擷取
        User user = userService.queryUserByUsername(token.getUsername());
        //user==null說明沒有找到
        if (user==null){
            //抛出UnknownAccountException異常,即使用者名不存在的異常
            return null;
        }else{
            //IncorrectCredentialsException異常,即密碼錯誤的異常交給Shiro來檢測
            return new SimpleAuthenticationInfo("",user.getPwd(),"");
        }
    }
               
  9. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,點選任意連結進入登入頁,輸入資料庫中存在的使用者名和密碼,登入成功!

11.6 Shiro 實作請求授權

  1. 修改

    ShiroConfig.java

    LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    //授權
    filterMap.put("/user/add", "perms[user:add]");
    //攔截
    filterMap.put("/user/*", "authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //設定登入請求
    bean.setLoginUrl("/toLogin");
               
  2. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,登入後點選 “修改使用者”,可以正常通路;點選 “新增使用者”,報出 401 錯誤。
  3. ShiroController.java

    中新增以下方法:
    @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized(){
        return "未經授權無法通路此頁面!";
    }
               
  4. 修改

    ShiroConfig.java

    LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    //授權
    filterMap.put("/user/add", "perms[user:add]");
    //攔截
    filterMap.put("/user/*", "authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //設定登入請求
    bean.setLoginUrl("/toLogin");
    //設定未授權頁面
    bean.setUnauthorizedUrl("/noauth");
               
  5. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,登入後點選 “新增使用者”,頁面輸出以下内容:
    未經授權無法通路此頁面!
               
  6. 修改

    UserRealm.java

    //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //對所有使用者進行授權
        info.addStringPermission("user:add");
        return info;
    }
               
  7. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,登入後點選 “新增使用者”,可以正常通路。
  8. 修改

    UserRealm.java

    //認證
    //IncorrectCredentialsException異常,即密碼錯誤的異常交給Shiro來檢測
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");
               
  9. 修改

    shiroConfig.java

    //授權
    filterMap.put("/user/add", "perms[user:add]");
    filterMap.put("/user/update", "perms[user:update]");
               
  10. 修改

    UserRealm.java

    //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //拿到目前登入的對象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal();
        //設定目前使用者的權限為資料庫中的權限
        //資料庫中有兩條資料:
        //id name pwd     perms
        // 1 root 123456  user:add
        // 2 wmwx 010101  user:update
        // 3 guest 222333 none
        info.addStringPermission(currentUser.getPerms());
        return info;
    }
               
  11. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,登入賬号 “root”,點選 “新增使用者,可以正常通路;點選 “修改使用者”,頁面輸出如下:
    未經授權無法通路此頁面!
               

11.7 Shiro 整合 Thymeleaf

  1. pom.xml

    中導入依賴:
    <!-- thymeleaf-shiro -->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
               
  2. 修改

    ShiroConfig.java

    //配置ShiroDialect:用來整合Shiro和Thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
               
  3. 修改

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:shiro="http://www.polix.at/thymeleaf/shiro">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
        <h1>首頁</h1>
        <a th:href="@{/toLogin}">登入</a>
        <a th:href="@{/}">登出</a>
        <hr />
        <a th:href="@{/user/add}" shiro:hasPermission="user:add">新增使用者</a>
        <a th:href="@{/user/update}" shiro:hasPermission="user:update">修改使用者</a>
    </body>
    </html>
               
  4. 打開浏覽器,在位址欄中輸入

    localhost:8080

    進入首頁,“登出”、“新增使用者” 和 “修改使用者” 的連結都未顯示;登入賬号 “root”,顯示 “新增使用者”;登陸賬号 “wmwx”,顯示 “修改使用者”;登入使用者 “guest”,兩個連結都不顯示;并且無論登入哪個賬号,“登入” 按鈕都會消失,“登出” 按鈕都會出現。

12. Swagger

12.1 簡介

  • Swagger 号稱是世界上最流行的 API 架構
  • Restful 風格的 Api 文檔線上自動生成
  • 直接運作,可以線上測試 API 接口
  • 支援多種語言(如:Java、PHP等)

官網:Swagger 官網

12.2 Spring Boot 內建 Swagger

  1. 建立一個 Spring Boot 項目,勾選 Spring Web
  2. 導入相關依賴(注意版本号,本處引用 2.9.2,從 3.0 開始 Swagger 進行了大更新)
    <!-- springfox-swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <!-- springfox-swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
               
  3. 建立

    config

    目錄,并在此目錄下建立

    SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2     //開啟Swagger2
    public class SwaggerConfig {
        
    }
               
  4. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,可以看到預設提供的文檔界面

12.3 Swagger 配置

12.3.1 配置 Swagger 資訊

  1. 補充

    SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2     //開啟Swagger2
    public class SwaggerConfig {
        //配置了 Swagger 的 Docket Bean 執行個體
        @Bean
        public Docket getDocket(){
            return new Docket(DocumentationType.SWAGGER_2);
        }
    }
               
  2. 繼續補充

    SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.Contact;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    import java.util.ArrayList;
    
    @Configuration
    @EnableSwagger2     //開啟Swagger2
    public class SwaggerConfig {
        //配置了 Swagger 的 Docket Bean 執行個體
        @Bean
        public Docket getDocket(){
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo());
        }
    
        //配置 Swagger 文檔資訊 => ApiInfo
        public ApiInfo apiInfo(){
            Contact contact = new Contact("妙霄", "mxblog.top", "[email protected]");
    
            return new ApiInfo(
                    "妙霄的Swagger文檔",
                    "騰蛟起鳳,孟學士之詞宗;紫電青霜,王将軍之武庫。",
                    "v1.0",
                    "mxblog.top",
                    contact,
                    "Apache 2.0",
                    "http://www.apache.org/licenses/LICENSE-2.0",
                    new ArrayList()
            );
        }
    }
               
  3. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,可以看到自己配置了個人資訊的文檔界面

12.3.2 配置 Swagger 掃描與過濾

  1. 建立

    controller

    目錄,并在其中建立

    HelloController.java

    檔案:
    package com.wmwx.springboot06.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
        @RequestMapping("/hello")
        public String hello(){
            return "Hello Swagger!";
        }
    }
               
  2. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,可以看到 Swagger 幫我們自動生成了兩個 Controller 接口文檔和一個 Models 文檔。
  3. 繼續補充

    SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 執行個體
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文檔資訊
                .apiInfo(apiInfo())
                .select()
                //RequestHandlerSelectors 配置掃描接口的方式
                //basePackage:指定要掃描的包(最常用)
                //any:全都掃描
                //none:都不掃描
                //withClassAnnotation:掃描類上的注解
                //withMethodAnnotation:掃描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                .build();
    }
               
  4. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,發現原本的文檔中顯示的兩個 Controller 和一個 Models,現在隻剩下一個

    hello-controller

    了。
  5. 繼續補充

    SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 執行個體
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文檔資訊
                .apiInfo(apiInfo())
                .select()
                //RequestHandlerSelectors 配置掃描接口的方式
                //basePackage:指定要掃描的包(最常用)
                //any:全都掃描
                //none:都不掃描
                //withClassAnnotation:掃描類上的注解
                //withMethodAnnotation:掃描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置過濾的路徑
                //ant:指定要過濾的路徑(最常用)
                //any:全都過濾
                //none:都不過濾
                .paths(PathSelectors.ant("/controller/**"))
                .build();
    }
               
  6. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,發現現在的文檔中隻顯示

    No operations defined in spec!

    ,即沒有自動生成的接口文檔了。
  7. 注釋掉第 5 步中新增的代碼,取消過濾,讓 Swagger 中隻生成

    controller

    包下接口的文檔。
  8. 繼續補充

    SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 執行個體
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文檔資訊
                .apiInfo(apiInfo())
                //定義是否啟動swagger,若為false則不能在浏覽器中通路
                .enable(false)
                .select()
                //RequestHandlerSelectors 配置掃描接口的方式
                //basePackage:指定要掃描的包(最常用)
                //any:全都掃描
                //none:都不掃描
                //withClassAnnotation:掃描類上的注解
                //withMethodAnnotation:掃描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置過濾的路徑
                //ant:指定要過濾的路徑(最常用)
                //any:全都過濾
                //none:都不過濾
                //.paths(PathSelectors.ant("/controller/**"))
                .build();
    }
               
  9. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,發現文檔不見了,頁面中有如下輸出:
    😱 Could not render e, see the console.
               

12.3.3 配置 Swagger 開關

  1. 删除項目中原本自動生成的

    application.properties

    檔案,并建立以下三個配置檔案:

    application-dev.yaml:

    # 應用服務 WEB 通路端口
    server:
      port: 8080
               
    application-prod.yaml:
    # 應用服務 WEB 通路端口
    server:
      port: 8082
               
    application.yaml:
    spring:
      # 應用名稱
      application:
        name: springboot-06
      # 使用的配置環境
      profiles:
        active: dev
               
    這樣一來,項目中就同時存在

    dev

    prod

    兩個配置環境了,并且此時正在使用的是

    dev

    環境。
  2. 修改

    SwaggerConfig.java

    (注意

    getDocket

    方法添加了參數):
    //配置了 Swagger 的 Docket Bean 執行個體
    @Bean
    public Docket getDocket(Environment environment){
        //設定要啟動Swagger的環境
        Profiles profile = Profiles.of("dev");
        //判斷目前環境是否是要啟動Swagger的環境
        boolean isEnv = environment.acceptsProfiles(profile);
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文檔資訊
                .apiInfo(apiInfo())
                //定義是否啟動swagger,若為false則不能在浏覽器中通路
                .enable(isEnv)
                .select()
                //RequestHandlerSelectors 配置掃描接口的方式
                //basePackage:指定要掃描的包(最常用)
                //any:全都掃描
                //none:都不掃描
                //withClassAnnotation:掃描類上的注解
                //withMethodAnnotation:掃描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置過濾的路徑
                //ant:指定要過濾的路徑(最常用)
                //any:全都過濾
                //none:都不過濾
                //.paths(PathSelectors.ant("/controller/**"))
                .build();
    }
               
    這樣一來,我們隻需要改變

    Profiles.of

    裡的參數,即可指定哪個環境才會去開啟 Swagger。

12.3.4 配置 Swagger 分組

在 Swagger 生成的文檔的右上角可以選擇分組,想要配置 Swagger 分組可以通過以下方式實作:

@Bean
public Docket getDocket(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("groupName");
}
           

而如果想要配置多個分組,也隻需編寫多個 Docket 即可:

//多個分組
@Bean
public Docket getDocketA(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket getDocketB(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket getDocketC(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}
           

12.4 Swagger 注解

12.4.1 實體類注解

  1. 建立

    pojo

    目錄,并在其中建立

    User.java

    ,并為

    User

    類添加

    @ApiModel

    注解,為其屬性添加

    @ApiModelProperty

    注解:
    package com.wmwx.springboot06.pojo;
    
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    
    @ApiModel("使用者類")
    public class User {
        @ApiModelProperty("編号")
        private int id;
        @ApiModelProperty("使用者名")
        private String name;
        @ApiModelProperty("密碼")
        private String pwd;
    
        public User() {
        }
    
        public User(int id, String name, String pwd) {
            this.id = id;
            this.name = name;
            this.pwd = pwd;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPwd() {
            return pwd;
        }
    
        public void setPwd(String pwd) {
            this.pwd = pwd;
        }
    }
               
  2. SwaggerController.java

    中添加一個方法,令其傳回值為

    User

    類型:
    @RequestMapping("/user")
    public User user(){
        return new User();
    }
               
  3. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,發現現在的文檔中顯示了

    Models

    子產品。點選展開後看到以下輸出内容:
    使用者類	{
        id	integer($int32)
        	編号
    
        name	string
        		使用者名
    
        pwd	string
        	密碼
    }
               
    由此可見,

    @ApiModel

    的作用是在文檔中為實體類添加注釋,而

    @ApiModelProperty

    則是為其屬性添加注釋。

    除此之外,還可以使用

    @Api

    注解,它與

    @ApiModel

    是等價的。

12.4.2 方法注解

  1. SwaggerController.java

    添加注解:
    @ApiOperation("hello方法")
    @RequestMapping("/hello")
    public String hello(@ApiParam("使用者名") String username){
        return "Hello " + username + "!";
    }
               
  2. 在浏覽器中輸入

    localhost:8080/swagger-ui.html

    ,可以看到文檔中的 “/hello” 旁邊被标注了 “hello方法”,展開參數清單可以看到 “使用者名” 的字樣。

13. 任務

13.1 異步任務

  1. 建立一個 Spring Boot 項目,勾選 Spring Web 即可
  2. 建立

    service

    目錄,并在其中建立

    AsyncService.java

    package com.wmwx.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class AsyncService {
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("資料正在處理......");
        }
    }
               
  3. 建立

    controller

    目錄,并在其中建立

    AsyncController.java

    package com.wmwx.controller;
    
    import com.wmwx.service.AsyncService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class AsyncController {
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping("/hello")
        public String hello(){
            asyncService.hello();
            return "方法執行完畢!";
        }
    }
               
  4. 在浏覽器中輸入

    localhost:8080/hello

    ,可以發現,需要等待3秒,控制台才會輸出 “資料處理完畢!”,螢幕上才會顯示出 “方法執行完畢!”的字樣。
  5. 修改

    AsyncService.java

    ,添加

    @Async

    注解:
    @Service
    public class AsyncService {
        //告訴Spring這是一個異步方法
        @Async
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("資料處理完畢!");
        }
    }
               
  6. 修改啟動類,添加

    @EnableAsync

    注解:
    @EnableAsync    //開啟異步注解
    @SpringBootApplication
    public class Springboot07Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot07Application.class, args);
        }
    
    }
               
  7. 在浏覽器中輸入

    localhost:8080/hello

    ,可以發現,頁面立即顯示出 “方法執行完畢!”,3秒後控制台才會輸出 “資料處理完畢! ”的字樣。

13.2 郵件任務

  1. pom.xml

    中導入依賴:
    <!-- mail -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
               
  2. application.properties

    中編寫郵箱配置:
    # 你的郵箱位址
    spring.mail.username=
    # 你的郵箱授權碼
    spring.mail.password=
    # smtp.163.com 或 smtp.qq.com 等
    spring.mail.host=
    # 如果是QQ郵箱,需要配置SSL開啟
    spring.mail.properties.mail.smtp.ssl.enable=true
               
  3. 編寫測試類,測試簡單郵件:
    @SpringBootTest
    class Springboot07ApplicationTests {
        @Autowired
        JavaMailSenderImpl mailSender;
        @Test
        void contextLoads() {
            //一個簡單的郵件
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("測試簡單郵件功能");
            message.setText("這裡是郵件的正文");
            message.setTo("");	//收信人位址
            message.setFrom("");	//寄信人位址
            mailSender.send(message);
        }
    }
               
  4. 運作測試類,打開收信人郵箱,就可以看到我們發出的郵件了。
  5. 在測試類中建立一個方法,測試複雜郵件:
    @Test
    void mailTest() throws MessagingException {
        //一個複雜的郵件
        MimeMessage message = mailSender.createMimeMessage();
        //組裝
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setSubject("測試複雜郵件功能");
        helper.setText("<p style='color:red;'>這裡是郵件的正文</p>", true);
        helper.setTo("");	//收信人位址
        helper.setFrom("");	//寄信人位址
        //附件
        helper.addAttachment("1.jpg", new File(""));	//檔案路徑,可以是本地的絕對路徑
        helper.addAttachment("2.jpg", new File(""));
        //發送
        mailSender.send(message);
    }
               
  6. 運作測試類,打開收信人郵箱,可以看到我們帶格式的郵件連帶附件一起發出了。

13.3 定時任務

  1. 在啟動類中添加

    @EnableScheduling

    注解:
    @EnableScheduling   //開啟定時功能的注解
    @SpringBootApplication
    public class Springboot07Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot07Application.class, args);
        }
    
    }
               
  2. service

    目錄下建立

    ScheduledService.java

    package com.wmwx.service;
    
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ScheduledService {
        //在一個特定的時間執行這個方法
        //cron表達式
        @Scheduled(cron = "0/2 * * * * ?")  //每兩秒執行一次
        public void hello(){
            System.out.println("Hello!");
        }
    }
               
  3. 運作啟動類(定時任務是異步的,隻要配置了自動就會運作,不需要額外去調用),在控制台中看到每兩秒輸出一次 “Hello!”

附錄 - cron 表達式示例:

(1)0/2 * * * * ?   表示每2秒 執行任務
(1)0 0/2 * * * ?   表示每2分鐘 執行任務
(1)0 0 2 1 * ?   表示在每月的1日的淩晨2點調整任務
(2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15執行作業
(3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每個月的最後一個星期五上午10:15執行作
(4)0 0 10,14,16 * * ?   每天上午10點,下午2點,4點
(5)0 0/30 9-17 * * ?   朝九晚五工作時間内每半小時
(6)0 0 12 ? * WED   表示每個星期三中午12點
(7)0 0 12 * * ?   每天中午12點觸發
(8)0 15 10 ? * *   每天上午10:15觸發
(9)0 15 10 * * ?     每天上午10:15觸發
(10)0 15 10 * * ?   每天上午10:15觸發
(11)0 15 10 * * ? 2005   2005年的每天上午10:15觸發
(12)0 * 14 * * ?     在每天下午2點到下午2:59期間的每1分鐘觸發
(13)0 0/5 14 * * ?   在每天下午2點到下午2:55期間的每5分鐘觸發
(14)0 0/5 14,18 * * ?     在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
(15)0 0-5 14 * * ?   在每天下午2點到下午2:05期間的每1分鐘觸發
(16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44觸發
(17)0 15 10 ? * MON-FRI   周一至周五的上午10:15觸發
(18)0 15 10 15 * ?   每月15日上午10:15觸發
(19)0 15 10 L * ?   每月最後一日的上午10:15觸發
(20)0 15 10 ? * 6L   每月的最後一個星期五上午10:15觸發
(21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最後一個星期五上午10:15觸發
(22)0 15 10 ? * 6#3   每月的第三個星期五上午10:15觸發
           

14. 分布式簡介

14.1 概念

分布式系統是若幹獨立計算機的集合,這些計算機對于使用者來說就像單個相關系統。它由一組通過網絡進行通信、為了完成共同的任務而協調工作的計算機節點組成。分布式系統的出現是為了用廉價的、普通的機器完成單個計算機無法完成的計算、存儲任務。其目的是通過利用更多的機器來處理更多的資料。

網際網路架構疊代:

  • 單一應用架構:當網站流量很小時,隻需通過一個應用将所有功能都部署在一起,以減少部署節點和成本。此時,用于簡化增删改查工作量的**資料通路架構(ORM)**是關鍵。這種架構适隻用于小型網站、小型管理系統,因為它将所有功能都部署到一個功能裡,簡單易用。但它的缺點也很明顯:性能擴充比較難、不易于協同開發、不利于更新維護等。
  • 垂直應用架構:當通路量逐漸增大時,單一應用增加機器帶來的加速度會越來越小,是以可以将應用拆成互不相幹的幾個應用,以提升效率。此時,用于加速前端頁面開發的 **Web 架構(MVC)**是關鍵。這種架構通過切分業務來實作各個子產品獨立部署,降低了維護和部署的難度,團隊各司其職更易管理,性能擴充也更友善,更有針對性。但同時,它的公用子產品無法重複利用,會造成開發性的浪費。
  • 分布式服務架構:當垂直應用越來越多時,應用之間互動不可避免。為了避免是以帶來的問題,可以将核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用于提高業務複用及整合的**分布式服務架構(RPC)**是關鍵。
  • 流動計算架構:當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個排程中心基于通路壓力實時管理叢集容量,提高叢集使用率。此時,用于提高機器使用率的資源排程和治理中心(SOA)[ Service Oriented Architecture ] 是關鍵。

14.2 RPC

RPC(Remote Procedure Call)是指遠端過程調用,是一種程序間通信方式。**它是一種技術思想,而不是規範。**它允許程式調用另一個位址空間(通常是共享網絡的另一台機器上)的方法,而不用程式員顯式編碼這個遠端調用的細節。即程式員無論是調用本地的還是遠端的方法,本質上編寫的調用代碼基本相同。

RPC 的兩個核心子產品是:

  1. 通訊
  2. 序列化

14.3 Dubbo

14.3.1 簡介

Apache Dubbo 是一款高性能、輕量級的開源 Java RPC 架構。它提供了三大核心能力:面向接口的遠端方法調用、智能容錯和負載均衡、服務自動注冊和發現。

Dubbo 官網:Dubbo 官網

14.3.2 基本構成

Dubbo 由以下幾大子產品構成:

  • 服務提供者(Provider):暴露服務的服務提供方。服務提供者在啟動時,向注冊中心注冊自己提供的服務。
  • 服務消費者(Consumer):調用遠端服務的服務消費方,服務消費者在啟動時,向注冊中心訂閱自己所需的服務。然後從提供者位址清單中,基于軟負載均衡算法,選一台提供者進行調用。如果調用失敗,再選另一台調用。
  • 注冊中心(Registry):注冊中心傳回服務提供者位址清單給消費者。如果有變更,注冊中心将基于長連接配接推送變更資料給消費者。
  • 監控中心(Monitor):服務消費者和服務提供者在記憶體中的累計調用次數和調用時間,會定時每分鐘發送一次統計資料到監控中心。