SpringBoot詳解

wydilearn發表於2022-04-28

1、Hello,World!

1.1、SpringBoot簡介

回顧什麼是Spring

Spring是一個開源框架,2003 年興起的一個輕量級的Java 開發框架,作者:Rod Johnson 。

Spring是為了解決企業級應用開發的複雜性而建立的,簡化開發。

Spring是如何簡化Java開發的

為了降低Java開發的複雜性,Spring採用了以下4種關鍵策略:

  1. 基於POJO的輕量級和最小侵入性程式設計,所有東西都是bean;

  2. 通過IOC,依賴注入(DI)和麵向介面實現鬆耦合;

  3. 基於切面(AOP)和慣例進行宣告式程式設計;

  4. 通過切面和模版減少樣式程式碼,RedisTemplate,xxxTemplate;

什麼是SpringBoot

學過javaweb的同學就知道,開發一個web應用,從最初開始接觸Servlet結合Tomcat, 跑出一個Hello Wolrld程式,是要經歷特別多的步驟;後來就用了框架Struts,再後來是SpringMVC,到了現在的SpringBoot,過一兩年又會有其他web框架出現;你們有經歷過框架不斷的演進,然後自己開發專案所有的技術也在不斷的變化、改造嗎?建議都可以去經歷一遍;

言歸正傳,什麼是SpringBoot呢,就是一個javaweb的開發框架,和SpringMVC類似,對比其他javaweb框架的好處,官方說是簡化開發,約定大於配置, you can "just run",能迅速的開發web應用,幾行程式碼開發一個http介面。

所有的技術框架的發展似乎都遵循了一條主線規律:從一個複雜應用場景 衍生 一種規範框架,人們只需要進行各種配置而不需要自己去實現它,這時候強大的配置功能成了優點;發展到一定程度之後,人們根據實際生產應用情況,選取其中實用功能和設計精華,重構出一些輕量級的框架;之後為了提高開發效率,嫌棄原先的各類配置過於麻煩,於是開始提倡“約定大於配置”,進而衍生出一些一站式的解決方案。

是的這就是Java企業級應用->J2EE->spring->springboot的過程。

隨著 Spring 不斷的發展,涉及的領域越來越多,專案整合開發需要配合各種各樣的檔案,慢慢變得不那麼易用簡單,違背了最初的理念,甚至人稱配置地獄。Spring Boot 正是在這樣的一個背景下被抽象出來的開發框架,目的為了讓大家更容易的使用 Spring 、更容易的整合各種常用的中介軟體、開源軟體;

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

簡單來說就是SpringBoot其實不是什麼新的框架,它預設配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

Spring Boot 出生名門,從一開始就站在一個比較高的起點,又經過這幾年的發展,生態足夠完善,Spring Boot 已經當之無愧成為 Java 領域最熱門的技術。

Spring Boot的主要優點:

  • 為所有Spring開發者更快的入門
  • 開箱即用,提供各種預設配置來簡化專案配置
  • 內嵌式容器簡化Web專案
  • 沒有冗餘程式碼生成和XML配置的要求

真的很爽,我們快速去體驗開發個介面的感覺吧!

1.2、Hello,World!

準備工作

我們將學習如何快速的建立一個Spring Boot應用,並且實現一個簡單的Http請求處理。通過這個例子對Spring Boot有一個初步的瞭解,並體驗其結構簡單、開發快速的特性。

我的環境準備:

  • java version "1.8.0_181"
  • Maven-3.6.1
  • SpringBoot 2.x 最新版

開發工具:

  • IDEA

建立基礎專案說明

Spring官方提供了非常方便的工具讓我們快速構建應用

Spring Initializr:https://start.spring.io/

專案建立方式一:使用Spring Initializr 的 Web頁面建立專案

  1. 開啟 https://start.spring.io/

  2. 填寫專案資訊

  3. 點選”Generate Project“按鈕生成專案;下載此專案

  4. 解壓專案包,並用IDEA以Maven專案匯入,一路下一步即可,直到專案匯入完畢。

  5. 如果是第一次使用,可能速度會比較慢,包比較多、需要耐心等待一切就緒。

專案建立方式二:使用 IDEA 直接建立專案

  1. 建立一個新專案

  2. 選擇spring initalizr , 可以看到預設就是去官網的快速構建工具那裡實現

  3. 填寫專案資訊

  4. 選擇初始化的元件(初學勾選 Web 即可)

  5. 填寫專案路徑

  6. 等待專案構建成功

專案結構分析:

通過上面步驟完成了基礎專案的建立。就會自動生成以下檔案。

  1. 程式的主啟動類

  2. 一個 application.properties 配置檔案

  3. 一個 測試類

  4. 一個 pom.xml

pom.xml分析

開啟pom.xml,看看SpringBoot專案的依賴:

<!-- 父依賴 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
    <relativePath/>
</parent>

<dependencies>
    <!-- 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>
        <!-- 剔除依賴 -->
        <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>

編寫一個http介面

  1. 在主程式的同級目錄下,新建一個controller包,一定要在同級目錄下,否則識別不到

  2. 在包中新建一個HelloController類

@RestController
public class HelloController {

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

}
  1. 編寫完畢後,從主程式啟動專案,瀏覽器發起請求,看頁面返回;控制檯輸出了 Tomcat 訪問的埠號!

簡單幾步,就完成了一個web介面的開發,SpringBoot就是這麼簡單。所以我們常用它來建立我們的微服務專案!

將專案打成jar包,點選maven的package

如果遇到錯誤,可以嘗試配置打包時,跳過專案執行測試用例

<!--
    在工作中,很多情況下我們打包是不想執行測試用例的
    可能是測試用例不完事,或是測試用例會影響資料庫資料
    跳過測試用例執
    -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <!--跳過專案執行測試用例-->
        <skipTests>true</skipTests>
    </configuration>
</plugin>

如果打包成功,則會在target目錄下生成一個 jar 包

打成了jar包後,就可以在任何地方執行了!OK

彩蛋

如何更改啟動時顯示的字元拼成的字母,SpringBoot呢?也就是 banner 圖案;

只需一步:到專案下的 resources 目錄下新建一個banner.txt 即可。

圖案可以到:https://www.bootschool.net/ascii 這個網站生成,然後拷貝到檔案中即可!

SpringBoot這麼簡單的東西背後一定有故事,我們之後去進行一波原始碼分析!

2、執行原理探究

2.1、pom.xml

父依賴

其中它主要是依賴一個父專案,主要是管理專案的資源過濾及外掛!

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

點進去,發現還有一個父依賴

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.6</version>
    <relativePath>../../spring-boot-dependencies</relativePath>
</parent>

這裡才是真正管理SpringBoot應用裡面所有依賴版本的地方,SpringBoot的版本控制中心;

以後我們匯入依賴預設是不需要寫版本;但是如果匯入的包沒有在依賴中管理著就需要手動配置版本了;

啟動器 spring-boot-starter

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

springboot-boot-starter-xxx:就是spring-boot的場景啟動器

spring-boot-starter-web:幫我們匯入了web模組正常執行所依賴的元件;

SpringBoot將所有的功能場景都抽取出來,做成一個個的starter (啟動器),只需要在專案中引入這些starter即可,所有相關的依賴都會匯入進來 , 我們要用什麼功能就匯入什麼樣的場景啟動器即可 ;我們未來也可以自己自定義 starter;

  • 啟動器:說白了就是SpringBoot的啟動場景;
  • 比如spring-boot-starter-web,就會幫我們自動匯入web環境所有的依賴!
  • SpringBoot會將所有的功能場景,都變成一個個的啟動器
  • 我們要使用什麼功能,就只需要找到對應的啟動器就可以了starter

2.2、主啟動類

分析完了 pom.xml 來看看這個啟動類

預設的主啟動類

//@SpringBootApplication 來標註一個主程式類
//說明這是一個Spring Boot應用
@SpringBootApplication
public class SpringbootApplication {

    public static void main(String[] args) {
        //以為是啟動了一個方法,沒想到啟動了一個服務
        SpringApplication.run(SpringbootApplication.class, args);
    }

}

但是一個簡單的啟動類並不簡單!我們來分析一下這些註解都幹了什麼

@SpringBootApplication

作用:標註在某個類上說明這個類是SpringBoot的主配置類 , SpringBoot就應該執行這個類的main方法來啟動SpringBoot應用;

進入這個註解:可以看到上面還有很多其他註解!

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
)
public @interface SpringBootApplication {
    // ......
}

@ComponentScan

這個註解在Spring中很重要 ,它對應XML配置中的元素。

作用:自動掃描並載入符合條件的元件或者bean , 將這個bean定義載入到IOC容器中

@SpringBootConfiguration

作用:SpringBoot的配置類 ,標註在某個類上 , 表示這是一個SpringBoot的配置類;

我們繼續進去這個註解檢視

// 點進去得到下面的 @Component
@Configuration
public @interface SpringBootConfiguration {}

@Component
public @interface Configuration {}

這裡的 @Configuration,說明這是一個配置類 ,配置類就是對應Spring的xml 配置檔案;

裡面的 @Component 這就說明,啟動類本身也是Spring中的一個元件而已,負責啟動應用!

我們回到 SpringBootApplication 註解中繼續看。

@EnableAutoConfiguration

@EnableAutoConfiguration :開啟自動配置功能

以前我們需要自己配置的東西,而現在SpringBoot可以自動幫我們配置 ;@EnableAutoConfiguration告訴SpringBoot開啟自動配置功能,這樣自動配置才能生效;

點進註解接續檢視:

@AutoConfigurationPackage :自動配置包

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}

@import :Spring底層註解@import , 給容器中匯入一個元件

Registrar.class 作用:將主啟動類的所在包及包下面所有子包裡面的所有元件掃描到Spring容器 ;

這個分析完了,退到上一步,繼續看

@Import({AutoConfigurationImportSelector.class}) :給容器匯入元件 ;

AutoConfigurationImportSelector :自動配置匯入選擇器,那麼它會匯入哪些元件的選擇器呢?我們點選去這個類看原始碼:

  1. 這個類中有一個這樣的方法
// 獲得候選的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    //這裡的getSpringFactoriesLoaderFactoryClass()方法
    //返回的就是我們最開始看的啟動自動匯入配置檔案的註解類;EnableAutoConfiguration
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
    return configurations;
}
  1. 這個方法又呼叫了 SpringFactoriesLoader 類的靜態方法!我們進入SpringFactoriesLoader類loadFactoryNames() 方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    //這裡它又呼叫了 loadSpringFactories 方法
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
  1. 我們繼續點選檢視 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    //獲得classLoader , 我們返回可以看到這裡得到的就是EnableAutoConfiguration標註的類本身
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        try {
            //去獲取一個資源 "META-INF/spring.factories"
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            //將讀取到的資源遍歷,封裝成為一個Properties
            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryClassName = ((String)entry.getKey()).trim();
                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String factoryName = var9[var11];
                        result.add(factoryClassName, factoryName.trim());
                    }
                }
            }

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}
  1. 發現一個多次出現的檔案:spring.factories,全域性搜尋它

spring.factories

我們根據源頭開啟spring.factories , 看到了很多自動配置的檔案;這就是自動配置根源所在!

WebMvcAutoConfiguration

我們在上面的自動配置類隨便找一個開啟看看,比如 :WebMvcAutoConfiguration

可以看到這些一個個的都是JavaConfig配置類,而且都注入了一些Bean,可以找一些自己認識的類,看著熟悉一下!

所以,自動配置真正實現是從classpath中搜尋所有的META-INF/spring.factories配置檔案 ,並將其中對應的 org.springframework.boot.autoconfigure. 包下的配置項,通過反射例項化為對應標註了 @Configuration的JavaConfig形式的IOC容器配置類 , 然後將這些都彙總成為一個例項並載入到IOC容器中。

結論:

  1. SpringBoot在啟動的時候,從類路徑下/META-INF/spring.factories 獲取指定的值;
  2. 將這些自動配置的類匯入容器,自動配置就會生效,幫我們進行自動配置!
  3. 以前我們需要自動配置的東西,現在SpringBoot幫我們做了!
  4. 整合javaEE,解決方案和自動配置的東西都在spring-boot-autoconfigure-2.6.6.jar這個包下
  5. 它會把所有需要匯入的元件,以類名的方式返回,這些元件就會被新增到容器;
  6. 容器中也會存在非常多的xxxAutoConfiguration的檔案(@Bean),就是這些類給容器中匯入了這個場景需要的所有元件;並自動配置,@Configuration,JavaConfig!
  7. 有了自動配置類,免去了我們手動編寫配置檔案的工作!

現在大家應該大概的瞭解了下,SpringBoot的執行原理,後面我們還會深化一次!

2.3、SpringApplication

不簡單的方法

我最初以為就是執行了一個main方法,沒想到卻開啟了一個服務;

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

SpringApplication.run分析

分析該方法主要分兩部分,一部分是SpringApplication的例項化,二是run方法的執行;

SpringApplication

這個類主要做了以下四件事情:

  1. 推斷應用的型別是普通的專案還是Web專案

  2. 查詢並載入所有可用初始化器 , 設定到initializers屬性中

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

  4. 推斷並設定main方法的定義類,找到執行的主類

檢視構造器:

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    // ......
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances();
                         this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
                         this.mainApplicationClass = this.deduceMainApplicationClass();
                         }

run方法流程分析

跟著原始碼和這幅圖就可以一探究竟了!

3、yaml配置注入

3.1、yaml語法學習

配置檔案

SpringBoot使用一個全域性的配置檔案 , 配置檔名稱是固定的

  • application.properties

    • 語法結構 :key=value
  • application.yml

    • 語法結構 :key:空格 value

配置檔案的作用 :修改SpringBoot自動配置的預設值,因為SpringBoot在底層都給我們自動配置好了;

比如我們可以在配置檔案中修改Tomcat 預設啟動的埠號!測試一下!

server:
  port: 8081

yaml概述

YAML是 "YAML Ain't a Markup Language" (YAML不是一種標記語言)的遞迴縮寫。在開發的這種語言時,YAML 的意思其實是:"Yet Another Markup Language"(仍是一種標記語言)

這種語言以資料作為中心,而不是以標記語言為重點!

以前的配置檔案,大多數都是使用xml來配置;比如一個簡單的埠配置,我們來對比下yaml和xml

傳統xml配置:

<server>
    <port>8081<port>
</server>

yaml配置:

server: 
  port: 8080

yaml基礎語法

說明:語法要求嚴格!

  1. 空格不能省略

  2. 以縮排來控制層級關係,只要是左邊對齊的一列資料都是同一個層級的。

  3. 屬性和值的大小寫都是十分敏感的。

字面量:普通的值 [ 數字,布林值,字串 ]

字面量直接寫在後面就可以 , 字串預設不用加上雙引號或者單引號;

k: v

注意:

  • “ ” 雙引號,不會轉義字串裡面的特殊字元 , 特殊字元會作為本身想表示的意思;

    比如 :name: "kuang \n shen" 輸出 :kuang 換行 shen

  • '' 單引號,會轉義特殊字元 , 特殊字元最終會變成和普通字元一樣輸出

    比如 :name: ‘kuang \n shen’ 輸出 :kuang \n shen

物件、Map(鍵值對)

#物件、Map格式
k: 
    v1:
    v2:

在下一行來寫物件的屬性和值得關係,注意縮排;比如:

student:
    name: qinjiang
    age: 3

行內寫法

student: {name: qinjiang,age: 3}

陣列( List、set )

用 - 值表示陣列中的一個元素,比如:

pets:
 - cat
 - dog
 - pig

行內寫法

pets: [cat,dog,pig]

修改SpringBoot的預設埠號

配置檔案中新增,埠號的引數,就可以切換埠;

server:
  port: 8082

3.2、注入配置檔案

yaml檔案更強大的地方在於,他可以給我們的實體類直接注入匹配值!

yaml注入配置檔案

  1. 在springboot專案中的resources目錄下新建一個檔案 application.yml

  2. 編寫一個實體類 Dog;

@Component  //註冊bean到容器中
public class Dog {
    private String name;
    private Integer age;

    //有參無參構造、get、set方法、toString()方法  
}
  1. 思考,我們原來是如何給bean注入屬性值的!@Value,給狗狗類測試一下:
@Component //註冊bean
public class Dog {
    @Value("阿黃")
    private String name;
    @Value("18")
    private Integer age;
}
  1. 在SpringBoot的測試類下注入狗狗輸出一下;
@SpringBootTest
class DemoApplicationTests {

    @Autowired //將狗狗自動注入進來
    private Dog dog;

    @Test
    public void contextLoads() {
        System.out.println(dog); //列印看下狗狗物件
    }

}

結果成功輸出,@Value注入成功,這是我們原來的辦法對吧。

  1. 我們在編寫一個複雜一點的實體類:Person 類
@Component //註冊bean到容器中
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;

    //有參無參構造、get、set方法、toString()方法  
}
  1. 我們來使用yaml配置的方式進行注入,大家寫的時候注意區別和優勢,我們編寫一個yaml配置!
person:
  name: qinjiang
  age: 3
  happy: false
  birth: 2022/04/08
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - music
    - girl
  dog:
    name: 大黃 # ${person.hello:hello}_旺財 如果person.hello有值,則注入person.hello的值,否則注入"hello";之後連線_旺財一起注入
    age: 3
  1. 我們剛才已經把person這個物件的所有值都寫好了,我們現在來注入到我們的類中!
/*
@ConfigurationProperties作用:
將配置檔案中配置的每一個屬性的值,對映到這個元件中;
告訴SpringBoot將本類中的所有屬性和配置檔案中相關的配置進行繫結
引數 prefix = “person” : 將配置檔案中的person下面的所有屬性一一對應
*/
@Component //註冊bean
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
}
  1. IDEA 提示,springboot配置註解處理器沒有找到,讓我們看文件,我們可以檢視文件,找到一個依賴!
<!-- 匯入配置檔案處理器,配置檔案進行繫結就會有提示,需要重啟 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  1. 確認以上配置都OK之後,我們去測試類中測試一下:
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    Person person; //將person自動注入進來

    @Test
    public void contextLoads() {
        System.out.println(person); //列印person資訊
    }

}

結果:所有值全部注入成功!

yaml配置注入到實體類完全OK!

載入指定的配置檔案

@PropertySource :載入指定的配置檔案;

@configurationProperties:預設從全域性配置檔案中獲取值;

  1. 我們去在resources目錄下新建一個person.properties檔案
name=qinjiang
  1. 然後在我們的程式碼中指定載入person.properties檔案
@PropertySource(value = "classpath:person.properties")
@Component //註冊bean
public class Person {

    @Value("${name}")
    private String name;

    ......  
}
  1. 再次輸出測試一下:指定配置檔案繫結成功!

配置檔案佔位符

配置檔案還可以編寫佔位符生成隨機數

person:
    name: qinjiang${random.uuid} # 隨機uuid
    age: ${random.int}  # 隨機int
    happy: false
    birth: 2000/01/01
    maps: {k1: v1,k2: v2}
    lists:
      - code
      - girl
      - music
    dog:
      name: ${person.hello:other}_旺財
      age: 1

回顧properties

我們上面採用的yaml方法都是最簡單的方式,開發中最常用的;也是springboot所推薦的!那我們來嘮嘮其他的實現方式,道理都是相同的;寫還是那樣寫;配置檔案除了yml還有我們之前常用的properties , 我們沒有講,我們來嘮嘮!

【注意】properties配置檔案在寫中文的時候,會有亂碼 , 我們需要去IDEA中設定編碼格式為UTF-8;

settings-->FileEncodings 中配置;

測試步驟:

  1. 新建一個實體類User
@Component //註冊bean
public class User {
    private String name;
    private int age;
    private String sex;
}
  1. 編輯配置檔案 user.properties
user1.name=kuangshen
user1.age=18
user1.sex=男
  1. 我們在User類上使用@Value來進行注入!
@Component //註冊bean
@PropertySource(value = "classpath:user.properties")
public class User {
    //直接使用@value
    @Value("${user.name}") //從配置檔案中取值
    private String name;
    @Value("#{9*2}")  // #{SPEL} Spring表示式
    private int age;
    @Value("男")  // 字面量
    private String sex;
}
  1. Springboot測試
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    User user;

    @Test
    public void contextLoads() {
        System.out.println(user);
    }

}

結果正常輸出!

對比小結

@Value這個使用起來並不友好!我們需要為每個屬性單獨註解賦值,比較麻煩;我們來看個功能對比圖

  1. @ConfigurationProperties只需要寫一次即可 , @Value則需要每個欄位都新增

  2. 鬆散繫結:這個什麼意思呢? 比如我的yml中寫的last-name,這個和lastName是一樣的, - 後面跟著的字母預設是大寫的。這就是鬆散繫結。可以測試一下

  3. JSR303資料校驗 , 這個就是我們可以在欄位是增加一層過濾器驗證 , 可以保證資料的合法性

  4. 複雜型別封裝,yml中可以封裝物件 , 使用value就不支援

結論:

配置yml和配置properties都可以獲取到值 , 強烈推薦 yml;

如果我們在某個業務中,只需要獲取配置檔案中的某個值,可以使用一下 @value;

如果說,我們專門編寫了一個JavaBean來和配置檔案進行一一對映,就直接@configurationProperties,不要猶豫!

4、JSR303資料校驗及多環境切換

4.1、JSR303資料校驗

先看看如何使用

在pom.xml中增加依賴

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

Springboot中可以用@validated來校驗資料,如果資料異常則會統一丟擲異常,方便異常中心統一處理。我們這裡來寫個註解讓我們的name只能支援Email格式;

@Component //註冊bean
@ConfigurationProperties(prefix = "person")
@Validated  //資料校驗
public class Person {

    @Email(message="郵箱格式錯誤") //name必須是郵箱格式
    private String name;
}

執行結果 :default message [不是一個合法的電子郵件地址];

使用資料校驗,可以保證資料的正確性;

常見引數

@NotNull(message="名字不能為空")
private String userName;
@Max(value=120,message="年齡最大不能查過120")
private int age;
@Email(message="郵箱格式錯誤")
private String 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.2、多環境切換

profile是Spring對不同環境提供不同配置功能的支援,可以通過啟用不同的環境版本,實現快速切換環境;

多配置檔案

我們在主配置檔案編寫的時候,檔名可以是 application-{profile}.properties/yml , 用來指定多個環境版本;

例如:

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

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

但是Springboot並不會直接啟動這些配置檔案,它預設使用application.properties主配置檔案

我們需要通過一個配置來選擇需要啟用的環境:

#比如在配置檔案中指定使用dev環境,我們可以通過設定不同的埠號進行測試;
#我們啟動SpringBoot,就可以看到已經切換到dev下的配置了;
spring.profiles.active=dev

yaml的多文件塊

和properties配置檔案中一樣,但是使用yml去實現不需要建立多個配置檔案,更加方便了!

server:
  port: 8081
#選擇要啟用那個環境塊
spring:
  profiles:
    active: prod

---
server:
  port: 8083
spring:
  profiles: dev #配置環境的名稱


---

server:
  port: 8084
spring:
  profiles: prod  #配置環境的名稱

注意:如果yml和properties同時都配置了埠,並且沒有啟用其他環境 , 預設會使用properties配置檔案的!

配置檔案載入位置

外部載入配置檔案的方式十分多,我們選擇最常用的即可,在開發的資原始檔中進行配置!

官方外部配置檔案說明參考文件

springboot 啟動會掃描以下位置的application.properties或者application.yml檔案作為Spring boot的預設配置檔案:

優先順序1:專案路徑下的config資料夾配置檔案
優先順序2:專案路徑下配置檔案
優先順序3:資源路徑下的config資料夾配置檔案
優先順序4:資源路徑下配置檔案

優先順序由高到底,高優先順序的配置會覆蓋低優先順序的配置;

SpringBoot會從這四個位置全部載入主配置檔案;互補配置;

我們在最低階的配置檔案中設定一個專案訪問路徑的配置來測試互補問題;

#配置專案的訪問路徑
server.servlet.context-path=/kuang

擴充,運維小技巧

指定位置載入配置檔案

我們還可以通過spring.config.location來改變預設的配置檔案位置

專案打包好以後,我們可以使用命令列引數的形式,啟動專案的時候來指定配置檔案的新位置;這種情況,一般是後期運維做的多,相同配置,外部指定的配置檔案優先順序最高

java -jar spring-boot-config.jar --spring.config.location=F:/application.properties

5、自動配置原理

配置檔案到底能寫什麼?怎麼寫?

分析自動配置原理

我們以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類中獲取的,這些類裡面的每一個屬性又是和配置檔案繫結的;
  • 所有在配置檔案中能配置的屬性都是在xxxxProperties類中封裝著;
  • 配置檔案能配置什麼就可以參照某個功能對應的這個屬性類
//從配置檔案中獲取指定的值和bean的屬性進行繫結
@ConfigurationProperties(prefix = "spring.http") 
public class HttpProperties {
    // .....
}

我們去配置檔案裡面試試字首,看提示!

這就是自動裝配的原理!

精髓

1、SpringBoot啟動會載入大量的自動配置類

2、我們看我們需要的功能有沒有在SpringBoot預設寫好的自動配置類當中;

3、我們再來看這個自動配置類中到底配置了哪些元件;(只要我們要用的元件存在在其中,我們就不需要再手動配置了)

4、給容器中自動配置類新增元件的時候,會從properties類中獲取某些屬性。我們只需要在配置檔案中指定這些屬性的值即可;

xxxxAutoConfigurartion:自動配置類;給容器中新增元件

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

瞭解:@Conditional

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

@Conditional派生註解(Spring註解版原生的@Conditional作用)

作用:必須是@Conditional指定的條件成立,才給容器中新增元件,配置配裡面的所有內容才生效;

@Conditional擴充套件註解 作用(判斷是否滿足當前指定條件)
@ConditionalOnJava 系統的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Be
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 滿足SpEL表示式指定
@ConditionalOnClass 系統中有指定的類
@ConditionalOnMissingClass 系統中沒有指定的類
@ConditionalOnSingleCandidate 容器中只有一個指定的Bean,或者這個Bean是首選Bean
@ConditionalOnProperty 系統中指定的屬性是否有指定的值
@ConditionalOnResource 類路徑下是否存在指定資原始檔
@ConditionalOnWebApplication 當前是web環境
@ConditionalOnNotWebApplication 當前不是web環境
@ConditionalOnJndi JNDI存在指定項

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

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

我們可以通過啟用 debug=true屬性;來讓控制檯列印自動配置報告,這樣我們就可以很方便的知道哪些自動配置類生效;

#開啟springboot的除錯類
debug=true

Positive matches:(自動配置類啟用的:正匹配)

Negative matches:(沒有啟動,沒有匹配成功的自動配置類:負匹配)

Unconditional classes: (沒有條件的類)

【演示:檢視輸出的日誌】

掌握吸收理解原理,即可以不變應萬變!

6、自定義Starter

我們分析完畢了原始碼以及自動裝配的過程,我們可以嘗試自定義一個啟動器來玩玩!

說明

啟動器模組是一個 空 jar 檔案,僅提供輔助性依賴管理,這些依賴可能用於自動裝配或者其他類庫;

命名歸約:

官方命名:

  • 字首:spring-boot-starter-xxx
  • 比如:spring-boot-starter-web....

自定義命名:

  • xxx-spring-boot-starter
  • 比如:mybatis-spring-boot-starter

編寫啟動器

1、在IDEA中新建一個空專案 spring-boot-starter-diy

2、新建一個普通Maven模組:kuang-spring-boot-starter

3、新建一個Springboot模組:kuang-spring-boot-starter-autoconfigure

4、點選apply即可,基本結構

5、在我們的 starter 中 匯入 autoconfigure 的依賴!

<!-- 啟動器 -->
<dependencies>
    <!--  引入自動配置模組 -->
    <dependency>
        <groupId>com.kuang</groupId>
        <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

6、將 autoconfigure 專案下多餘的檔案都刪掉,Pom中只留下一個 starter,這是所有的啟動器基本配置!

7、我們編寫一個自己的服務

public class HelloService {

    HelloProperties helloProperties;

    public HelloProperties getHelloProperties() {
        return helloProperties;
    }

    public void setHelloProperties(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    public String sayHello(String name){
        return helloProperties.getPrefix() + name + helloProperties.getSuffix();
    }

}

8、編寫HelloProperties 配置類

// 字首 kuang.hello
@ConfigurationProperties(prefix = "kuang.hello")
public class HelloProperties {

    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}

9、編寫我們的自動配置類並注入bean,測試!

@Configuration
@ConditionalOnWebApplication //web應用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloProperties helloProperties;

    @Bean
    public HelloService helloService(){
        HelloService service = new HelloService();
        service.setHelloProperties(helloProperties);
        return service;
    }
}

10、在resources編寫一個自己的 META-INF\spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.kuang.HelloServiceAutoConfiguration

11、編寫完成後,可以安裝到maven倉庫中!

新建專案測試我們自己寫的啟動器

1、新建一個SpringBoot 專案

2、匯入我們自己寫的啟動器

<dependency>
    <groupId>com.kuang</groupId>
    <artifactId>kuang-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3、編寫一個 HelloController 進行測試我們自己的寫的介面!

@RestController
public class HelloController {

    @Autowired
    HelloService helloService;

    @RequestMapping("/hello")
    public String hello(){
        return helloService.sayHello("zxc");
    }

}

4、編寫配置檔案 application.properties

kuang.hello.prefix="ppp"
kuang.hello.suffix="sss"

5、啟動專案進行測試,結果成功 !

7、Web開發靜態資源處理

7.1、Web開發探究

簡介

好的,同學們,那麼接下來呢,我們開始學習SpringBoot與Web開發,從這一章往後,就屬於我們實戰部分的內容了;

其實SpringBoot的東西用起來非常簡單,因為SpringBoot最大的特點就是自動裝配。

使用SpringBoot的步驟:

1、建立一個SpringBoot應用,選擇我們需要的模組,SpringBoot就會預設將我們的需要的模組自動配置好

2、手動在配置檔案中配置部分配置專案就可以執行起來了

3、專注編寫業務程式碼,不需要考慮以前那樣一大堆的配置了。

要熟悉掌握開發,之前學習的自動配置的原理一定要搞明白!

比如SpringBoot到底幫我們配置了什麼?我們能不能修改?我們能修改哪些配置?我們能不能擴充套件?

  • 向容器中自動配置元件 :*** Autoconfiguration
  • 自動配置類,封裝配置檔案的內容:***Properties

沒事就找找類,看看自動裝配原理!

我們之後來進行一個單體專案的小專案測試,讓大家能夠快速上手開發!

7.2、靜態資源處理

靜態資源對映規則

首先,我們搭建一個普通的SpringBoot專案,回顧一下HelloWorld程式!

寫請求非常簡單,那我們要引入我們前端資源,我們專案中有許多的靜態資源,比如css,js等檔案,這個SpringBoot怎麼處理呢?

如果我們是一個web應用,我們的main下會有一個webapp,我們以前都是將所有的頁面導在這裡面的,對吧!但是我們現在的pom呢,打包方式是為jar的方式,那麼這種方式SpringBoot能不能來給我們寫頁面呢?當然是可以的,但是SpringBoot對於靜態資源放置的位置,是有規定的!

我們先來聊聊這個靜態資源對映規則:

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/** , 都需要去 classpath:/META-INF/resources/webjars/ 找對應的資源;

什麼是webjars呢?

Webjars本質就是以jar包的方式引入我們的靜態資源 , 我們以前要匯入一個靜態資原始檔,直接匯入即可。

使用SpringBoot需要使用Webjars,我們可以去搜尋一下:

網站:https://www.webjars.org

要使用jQuery,我們只要要引入jQuery對應版本的pom依賴即可!

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

匯入完畢,檢視webjars目錄結構,並訪問Jquery.js檔案!

訪問:只要是靜態資源,SpringBoot就會去對應的路徑尋找資源,我們這裡訪問:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

第二種靜態資源對映規則

那我們專案中要是使用自己的靜態資源該怎麼匯入呢?我們看下一行程式碼;

我們去找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:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

我們可以在resources根目錄下新建對應的資料夾,都可以存放我們的靜態檔案;

比如我們訪問 http://localhost:8080/1.js , 他就會去這些資料夾中尋找對應的靜態資原始檔;

自定義靜態資源路徑

我們也可以自己通過配置檔案來指定一下,哪些資料夾是需要我們放靜態資原始檔的,在application.properties中配置;

spring.resources.static-locations=classpath:/coding/,classpath:/kuang/

一旦自己定義了靜態資料夾的路徑,原來的自動配置就都會失效了!

7.3、首頁處理

靜態資原始檔夾說完後,我們繼續向下看原始碼!可以看到一個歡迎頁的對映,就是我們的首頁!

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

點進去繼續看

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 頁面;被 /** 對映。

比如我訪問 http://localhost:8080/ ,就會找靜態資原始檔夾下的 index.html

新建一個 index.html ,在我們上面的3個目錄中任意一個;然後訪問測試 http://localhost:8080/ 看結果!

關於網站圖示說明

與其他靜態資源一樣,Spring Boot在配置的靜態內容位置中查詢 favicon.ico。如果存在這樣的檔案,它將自動用作應用程式的favicon。

1、關閉SpringBoot預設圖示

#關閉預設圖示
spring.mvc.favicon.enabled=false

2、自己放一個圖示在靜態資源目錄下,我放在 public 目錄下

3、清除瀏覽器快取!重新整理網頁,發現圖示已經變成自己的了!

8、Thymeleaf模板引擎

模板引擎

前端交給我們的頁面,是html頁面。如果是我們以前開發,我們需要把他們轉成jsp頁面,jsp好處就是當我們查出一些資料轉發到JSP頁面以後,我們可以用jsp輕鬆實現資料的顯示,及互動等。

jsp支援非常強大的功能,包括能寫Java程式碼,但是呢,我們現在的這種情況,SpringBoot這個專案首先是以jar的方式,不是war,像第二,我們用的還是嵌入式的Tomcat,所以呢,他現在預設是不支援jsp的

那不支援jsp,如果我們直接用純靜態頁面的方式,那給我們開發會帶來非常大的麻煩,那怎麼辦呢?

SpringBoot推薦你可以來使用模板引擎:

模板引擎,我們其實大家聽到很多,其實jsp就是一個模板引擎,還有用的比較多的freemarker,包括SpringBoot給我們推薦的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他們的思想都是一樣的,什麼樣一個思想呢我們來看一下這張圖:

模板引擎的作用就是我們來寫一個頁面模板,比如有些值呢,是動態的,我們寫一些表示式。而這些值,從哪來呢,就是我們在後臺封裝一些資料。然後把這個模板和這個資料交給我們模板引擎,模板引擎按照我們這個資料幫你把這表示式解析、填充到我們指定的位置,然後把這個資料最終生成一個我們想要的內容給我們寫出去,這就是我們這個模板引擎,不管是jsp還是其他模板引擎,都是這個思想。只不過呢,就是說不同模板引擎之間,他們可能這個語法有點不一樣。其他的我就不介紹了,我主要來介紹一下SpringBoot給我們推薦的Thymeleaf模板引擎,這模板引擎呢,是一個高階語言的模板引擎,他的這個語法更簡單。而且呢,功能更強大。

我們呢,就來看一下這個模板引擎,那既然要看這個模板引擎。首先,我們來看SpringBoot裡邊怎麼用。

引入Thymeleaf

怎麼引入呢,對於springboot來說,什麼事情不都是一個start的事情嘛,我們去在專案中引入一下。給大家三個網址:

Thymeleaf 官網:https://www.thymeleaf.org/

Thymeleaf 在Github 的主頁:https://github.com/thymeleaf/thymeleaf

Spring官方文件:找到我們對應的版本

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

找到對應的pom依賴:可以適當點進原始碼看下本來的包!

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

Maven會自動下載jar包,我們可以去看下下載的東西;

Thymeleaf分析

前面呢,我們已經引入了Thymeleaf,那這個要怎麼使用呢?

我們首先得按照SpringBoot的自動配置原理看一下我們這個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;
}

我們可以在其中看到預設的字首和字尾!

我們只需要把我們的html頁面放在類路徑下的templates下,thymeleaf就可以幫我們自動渲染了。

使用thymeleaf什麼都不需要配置,只需要將他放在指定的資料夾下即可!

測試:

1、編寫一個TestController

@Controller
public class TestController {
    
    @RequestMapping("/t1")
    public String test1(){
        //classpath:/templates/test.html
        return "test";
    }
    
}

2、編寫一個測試頁面 test.html 放在 templates 目錄下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>測試頁面</h1>

</body>
</html>

3、啟動專案請求測試

Thymeleaf語法學習

要學習語法,還是參考官網文件最為準確,我們找到對應的版本看一下;

Thymeleaf 官網:https://www.thymeleaf.org/ , 簡單看一下官網!我們去下載Thymeleaf的官方文件!

我們做個最簡單的練習 :我們需要查出一些資料,在頁面中展示

1、修改測試請求,增加資料傳輸;

@RequestMapping("/t1")
public String test1(Model model){
    //存入資料
    model.addAttribute("msg","Hello,Thymeleaf");
    //classpath:/templates/test.html
    return "test";
}

2、我們要使用thymeleaf,需要在html檔案中匯入名稱空間的約束,方便提示。

我們可以去官方文件的#3中看一下名稱空間拿來過來:

xmlns:th="http://www.thymeleaf.org"

3、我們去編寫下前端頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神說</title>
</head>
<body>
<h1>測試頁面</h1>

<!--th:text就是將div中的內容設定為它指定的值,和之前學習的Vue一樣-->
<div th:text="${msg}"></div>
</body>
</html>

4、啟動測試!

OK,入門搞定,我們來認真研習一下Thymeleaf的使用語法!

1、我們可以使用任意的 th:attr 來替換Html中原生屬性的值!

2、我們能寫哪些表示式呢?

Simple expressions:(表示式語法)
Variable Expressions: ${...}:獲取變數值;OGNL;
    1)、獲取物件的屬性、呼叫方法
    2)、使用內建的基本物件:#18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.

    3)、內建的一些工具物件:
      #execInfo : information about the template being processed.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any).
      #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
==================================================================================

  Selection Variable Expressions: *{...}:選擇表示式:和${}在功能上是一樣;
  Message Expressions: #{...}:獲取國際化內容
  Link URL Expressions: @{...}:定義URL;
  Fragment Expressions: ~{...}:片段引用表示式

Literals(字面量)
      Text literals: 'one text' , 'Another one!' ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…
      
Text operations:(文字操作)
    String concatenation: +
    Literal substitutions: |The name is ${name}|
    
Arithmetic operations:(數學運算)
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
    
Boolean operations:(布林運算)
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
    
Comparisons and equality:(比較運算)
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
    
Conditional operators:條件運算(三元運算子)
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
    
Special tokens:
    No-Operation: _

練習測試:

1、 我們編寫一個Controller,放一些資料

@RequestMapping("/t2")
public String test2(Map<String,Object> map){
    //存入資料
    map.put("msg","<h1>Hello</h1>");
    map.put("users", Arrays.asList("qinjiang","kuangshen"));
    //classpath:/templates/test.html
    return "test";
}

2、測試頁面取出資料

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神說</title>
</head>
<body>
<h1>測試頁面</h1>

<div th:text="${msg}"></div>
<!--不轉義-->
<div th:utext="${msg}"></div>

<!--遍歷資料-->
<!--th:each每次遍歷都會生成當前這個標籤:官網#9-->
<h4 th:each="user :${users}" th:text="${user}"></h4>

<h4>
    <!--行內寫法:官網#12-->
    <span th:each="user:${users}">[[${user}]]</span>
</h4>

</body>
</html>

3、啟動專案測試!

我們看完語法,很多樣式,我們即使現在學習了,也會忘記,所以我們在學習過程中,需要使用什麼,根據官方文件來查詢,才是最重要的,要熟練使用官方文件!

9、MVC自動配置原理

官網閱讀

在進行專案編寫前,我們還需要知道一個東西,就是SpringBoot對我們的SpringMVC還做了哪些配置,包括如何擴充套件,如何定製。

只有把這些都搞清楚了,我們在之後使用才會更加得心應手。途徑一:原始碼分析,途徑二:官方文件!

地址 :https://docs.spring.io/spring-boot/docs/2.6.6/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

Spring MVC Auto-configuration
// Spring Boot為Spring MVC提供了自動配置,它可以很好地與大多數應用程式一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自動配置在Spring預設設定的基礎上新增了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
// 包含檢視解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支援靜態資原始檔夾的路徑,以及webjars
Support for serving static resources, including support for WebJars 
// 自動註冊了Converter:
// 轉換器,這就是我們網頁提交資料到後臺自動封裝成為物件的東西,比如把"1"字串自動轉換為int型別
// Formatter:【格式化器,比如頁面給我們了一個2019-8-10,它會給我們自動格式化為Date物件】
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters
// SpringMVC用來轉換Http請求和響應的的,比如我們要把一個User物件轉換為JSON字串,可以去看官網文件解釋;
Support for HttpMessageConverters (covered later in this document).
// 定義錯誤程式碼生成規則的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首頁定製
Static index.html support.
// 圖示定製
Custom Favicon support (covered later in this document).
// 初始化資料繫結器:幫我們把請求資料繫結到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

/*
如果您希望保留Spring Boot MVC功能,並且希望新增其他MVC配置(攔截器、格式化程式、檢視控制器和其他功能),則可以新增自己
的@configuration類,型別為webmvcconfiguer,但不新增@EnableWebMvc。如果希望提供
RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定義
例項,則可以宣告WebMVCregistrationAdapter例項來提供此類元件。
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration 
(interceptors, formatters, view controllers, and other features), you can add your own 
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide 
custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or 
ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

// 如果您想完全控制Spring MVC,可以新增自己的@Configuration,並用@EnableWebMvc進行註釋。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

我們來仔細對照,看一下它怎麼實現的,它告訴我們SpringBoot已經幫我們自動配置好了SpringMVC,然後自動配置了哪些東西呢?

ContentNegotiatingViewResolver 內容協商檢視解析器

自動配置了ViewResolver,就是我們之前學習的SpringMVC的檢視解析器;

即根據方法的返回值取得檢視物件(View),然後由檢視物件決定如何渲染(轉發,重定向)。

我們去看看這裡的原始碼:我們找到 WebMvcAutoConfiguration , 然後搜尋ContentNegotiatingViewResolver。找到如下方法!

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver使用所有其他檢視解析器來定位檢視,因此它應該具有較高的優先順序
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}

我們可以點進這類看看!找到對應的解析檢視的程式碼;

@Nullable // 註解說明:@Nullable 即引數可為null
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
    if (requestedMediaTypes != null) {
        // 獲取候選的檢視物件
        List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
        // 選擇一個最適合的檢視物件,然後把這個物件返回
        View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }
    // .....
}

我們繼續點進去看,他是怎麼獲得候選的檢視的呢?

getCandidateViews中看到他是把所有的檢視解析器拿來,進行while迴圈,挨個解析!

Iterator var5 = this.viewResolvers.iterator();

所以得出結論:ContentNegotiatingViewResolver 這個檢視解析器就是用來組合所有的檢視解析器的

我們再去研究下他的組合邏輯,看到有個屬性viewResolvers,看看它是在哪裡進行賦值的!

protected void initServletContext(ServletContext servletContext) {
    // 這裡它是從beanFactory工具中獲取容器中的所有檢視解析器
    // ViewRescolver.class 把所有的檢視解析器來組合的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
    }
    // ...............
}

既然它是在容器中去找檢視解析器,我們是否可以猜想,我們就可以去實現一個檢視解析器了呢?

我們可以自己給容器中去新增一個檢視解析器;這個類就會幫我們自動的將它組合進來;我們去實現一下

1、我們在我們的主程式中去寫一個檢視解析器來試試;

@Bean //放到bean中
public ViewResolver myViewResolver(){
    return new MyViewResolver();
}

//我們寫一個靜態內部類,檢視解析器就需要實現ViewResolver介面
private static class MyViewResolver implements ViewResolver{
    @Override
    public View resolveViewName(String s, Locale locale) throws Exception {
        return null;
    }
}

2、怎麼看我們自己寫的檢視解析器有沒有起作用呢?

我們給 DispatcherServlet 中的 doDispatch方法 加個斷點進行除錯一下,因為所有的請求都會走到這個方法中

3、我們啟動我們的專案,然後隨便訪問一個頁面,看一下Debug資訊;

找到this

找到檢視解析器,我們看到我們自己定義的就在這裡了;

所以說,我們如果想要使用自己定製化的東西,我們只需要給容器中新增這個元件就好了!剩下的事情SpringBoot就會幫我們做了!

轉換器和格式化器

找到格式化轉換器:

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 拿到配置檔案中的格式化規則
    WebConversionService conversionService = 
        new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}

點進去:

public String getDateFormat() {
    return this.dateFormat;
}

/**
* Date format to use. For instance, `dd/MM/yyyy`. 預設的
 */
private String dateFormat;

可以看到在我們的Properties檔案中,我們可以進行自動配置它!

如果配置了自己的格式化方式,就會註冊到Bean中生效,我們可以在配置檔案中配置日期格式化的規則:

spring.mvc.date-format=

其餘的就不一一舉例了,大家可以下去多研究探討即可!

修改SpringBoot的預設配置

這麼多的自動配置,原理都是一樣的,通過這個WebMVC的自動配置原理分析,我們要學會一種學習方式,通過原始碼探究,得出結論;這個結論一定是屬於自己的,而且一通百通。

SpringBoot的底層,大量用到了這些設計細節思想,所以,沒事需要多閱讀原始碼!得出結論;

SpringBoot在自動配置很多元件的時候,先看容器中有沒有使用者自己配置的(如果使用者自己配置@bean),如果有就用使用者配置的,如果沒有就用自動配置的;

如果有些元件可以存在多個,比如我們的檢視解析器,就將使用者配置的和自己預設的組合起來!

擴充套件使用SpringMVC 官方文件如下:

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

我們要做的就是編寫一個@Configuration註解類,並且型別要為WebMvcConfigurer,還不能標註@EnableWebMvc註解;我們去自己寫一個;我們新建一個包叫config,寫一個類MyMvcConfig;

//應為型別要求為WebMvcConfigurer,所以我們實現其介面
//可以使用自定義類擴充套件MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 瀏覽器傳送/test , 就會跳轉到test頁面;
        registry.addViewController("/test").setViewName("test");
    }
}

我們去瀏覽器訪問一下:

確實也跳轉過來了!所以說,我們要擴充套件SpringMVC,官方就推薦我們這麼去使用,既保SpringBoot留所有的自動配置,也能用我們擴充套件的配置!

我們可以去分析一下原理:

1、WebMvcAutoConfiguration 是 SpringMVC的自動配置類,裡面有一個類WebMvcAutoConfigurationAdapter

2、這個類上有一個註解,在做其他自動配置時會匯入:@Import(EnableWebMvcConfiguration.class)

3、我們點進EnableWebMvcConfiguration這個類看一下,它繼承了一個父類:DelegatingWebMvcConfiguration

這個父類中有這樣一段程式碼:

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
  // 從容器中獲取所有的webmvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
}

4、我們可以在這個類中去尋找一個我們剛才設定的viewController當做參考,發現它呼叫了一個

protected void addViewControllers(ViewControllerRegistry registry) {
    this.configurers.addViewControllers(registry);
}

5、我們點進去看一下

public void addViewControllers(ViewControllerRegistry registry) {
    Iterator var2 = this.delegates.iterator();

    while(var2.hasNext()) {
        // 將所有的WebMvcConfigurer相關配置來一起呼叫!包括我們自己配置的和Spring給我們配置的
        WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
        delegate.addViewControllers(registry);
    }

}

所以得出結論:所有的WebMvcConfiguration都會被作用,不止Spring自己的配置類,我們自己的配置類當然也會被呼叫;

全面接管SpringMVC

官方文件:

If you want to take complete control of Spring MVC
you can add your own @Configuration annotated with @EnableWebMvc.

全面接管即:SpringBoot對SpringMVC的自動配置不需要了,所有都是我們自己去配置!

只需在我們的配置類中要加一個@EnableWebMvc。

我們看下如果我們全面接管了SpringMVC了,我們之前SpringBoot給我們配置的靜態資源對映一定會無效,我們可以去測試一下;

不加註解之前,訪問首頁:正常訪問

給配置類加上註解:@EnableWebMvc

我們發現所有的SpringMVC自動配置都失效了!迴歸到了最初的樣子;

當然,我們開發中,不推薦使用全面接管SpringMVC

思考問題?為什麼加了一個註解,自動配置就失效了!我們看下原始碼:

1、這裡發現它是匯入了一個類,我們可以繼續進去看

@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

2、它繼承了一個父類 WebMvcConfigurationSupport

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  // ......
}

3、我們來回顧一下Webmvc自動配置類

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 這個註解的意思就是:容器中沒有這個元件的時候,這個自動配置類才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
}

總結一句話:@EnableWebMvc將WebMvcConfigurationSupport元件匯入進來了;

而匯入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

在SpringBoot中會有非常多的擴充套件配置,只要看見了這個,我們就應該多留心注意~

10、員工管理系統

  1. 首頁配置:
    1. 注意點,所有頁面的靜態資源都需要使用thymeleaf接管;
    2. url:@{}
  2. 頁面國際化:
    1. 我們需要配置i18n檔案
    2. 我們如果需要在專案中進行按鈕自動切換,我們需要自定義一個元件LocaleResolver
    3. 記得將自己寫的元件配置到spring容器中@Bean
    4. {}

  3. 登入 + 攔截器
  4. 員工頁面展示
    1. 提取公共頁面
      1. th:fragment="sidebar"
      2. th:replace="~{commons/commons::topbar}"
      3. 如果要傳遞引數,可以直接使用()傳參,接受判斷即可!
    2. 列表迴圈展示
  5. 新增員工
    1. 按鈕提交
    2. 跳轉到新增頁面
    3. 新增員工成功
    4. 返回首頁
  6. CRUD搞定
  7. 404,在templates資料夾下新建error資料夾,命名指定的404頁面為404.html
# 1.前端搞定:頁面長什麼樣子:資料
# 2.設計資料庫(資料庫設計難點!)
# 3.前端讓他能夠自動執行,獨立化工程
# 4.資料介面如何對接:json,物件 all in one!
# 5.前後端聯調測試!
1.有一套自己熟悉的後臺模板:工作必要!x-admin
2.前端介面:至少自己能夠通過前端框架,組合出來一個網站頁面
	- index
	- about
	- blog
	- post
	- user
3.讓這個網站能夠獨立執行!
一個月!

10.1、首頁實現

首頁配置:注意點,所有頁面的靜態資源都需要使用thymeleaf接管;@{}

  1. 匯入thymeleaf依賴

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  2. 設定主頁訪問域名,新建config資料夾

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        //設定主頁訪問域名
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    }
    
  3. 在properties配置檔案中關閉模板引擎快取,並設定伺服器路徑

    # 關閉模板引擎的快取
    spring.thymeleaf.cache=false
    
    # 設定伺服器路徑
    server.servlet.context-path=/kuang
    
  4. 使用thymeleaf模板引擎,設定html靜態資原始檔

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    	<head>
    		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    		<meta name="description" content="">
    		<meta name="author" content="">
    		<title>Signin Template for Bootstrap</title>
    		<!-- Bootstrap core CSS -->
    		<link th:href="@{css/bootstrap.min.css}" rel="stylesheet">
    		<!-- Custom styles for this template -->
    		<link th:href="@{css/signin.css}" rel="stylesheet">
    	</head>
    
    	<body class="text-center">
    		<form class="form-signin" action="dashboard.html">
    			<img class="mb-4" th:src="@{img/bootstrap-solid.svg}" alt="" width="72" height="72">
    			<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    			<label class="sr-only">Username</label>
    			<input type="text" class="form-control" placeholder="Username" required="" autofocus="">
    			<label class="sr-only">Password</label>
    			<input type="password" class="form-control" placeholder="Password" required="">
    			<div class="checkbox mb-3">
    				<label>
              <input type="checkbox" value="remember-me"> Remember me
            </label>
    			</div>
    			<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    			<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
    			<a class="btn btn-sm">中文</a>
    			<a class="btn btn-sm">English</a>
    		</form>
    
    	</body>
    
    </html>
    <!--===================================================================-->
    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    	<head>
    		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    		<meta name="description" content="">
    		<meta name="author" content="">
    
    		<title>Dashboard Template for Bootstrap</title>
    		<!-- Bootstrap core CSS -->
    		<link th:href="@{css/bootstrap.min.css}" rel="stylesheet">
    
    		<!-- Custom styles for this template -->
    		<link th:href="@{css/dashboard.css}" rel="stylesheet">
        <head>
    <html>
    <!--===================================================================-->
    <!--其餘檔案類似-->
    ......
    

10.2、頁面國際化

有的時候,我們的網站會去涉及中英文甚至多語言的切換,這時候我們就需要學習國際化了!

準備工作

先在IDEA中統一設定properties的編碼問題!

編寫國際化配置檔案,抽取頁面需要顯示的國際化頁面訊息。我們可以去登入頁面檢視一下,哪些內容我們需要編寫國際化的配置!

配置檔案編寫

1、我們在resources資原始檔下新建一個i18n目錄,存放國際化配置檔案

2、建立一個login.properties檔案,還有一個login_zh_CN.properties;發現IDEA自動識別了我們要做國際化操作;資料夾變了!

3、我們可以在這上面去新建一個檔案;

我們再新增一個英文的;

這樣就快捷多了!

在IDEA外掛中搜尋Resource Bundle Editor並安裝!

4、接下來,我們就來編寫配置,我們可以看到idea下面有另外一個檢視;

這個檢視我們點選 + 號就可以直接新增屬性了;我們新建一個login.tip,可以看到邊上有三個檔案框可以輸入

我們新增一下首頁的內容!

然後依次新增其他頁面內容即可!

然後去檢視我們的配置檔案;

login.properties :預設

login.btn=請登入
login.password=密碼
login.remember=記住我
login.tip=請登入
login.username=使用者名稱

英文:

login.btn=Please sign in
login.password=password
login.remember=Remember me
login.tip=Please sign in
login.username=UserName

中文:

login.btn=請登入
login.password=密碼
login.remember=記住我
login.tip=請登入
login.username=使用者名稱

OK,配置檔案步驟搞定!

配置檔案生效探究

我們去看一下SpringBoot對國際化的自動配置!這裡又涉及到一個類:MessageSourceAutoConfiguration

裡面有一個方法,這裡發現SpringBoot已經自動配置好了管理我們國際化資原始檔的元件 ResourceBundleMessageSource;

// 獲取 properties 傳遞過來的值進行判斷
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    if (StringUtils.hasText(properties.getBasename())) {
        // 設定國際化檔案的基礎名(去掉語言國家程式碼的)
        messageSource.setBasenames(
            StringUtils.commaDelimitedListToStringArray(
                                       StringUtils.trimAllWhitespace(properties.getBasename())));
    }
    if (properties.getEncoding() != null) {
        messageSource.setDefaultEncoding(properties.getEncoding().name());
    }
    messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
    Duration cacheDuration = properties.getCacheDuration();
    if (cacheDuration != null) {
        messageSource.setCacheMillis(cacheDuration.toMillis());
    }
    messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
    messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
    return messageSource;
}

我們真實 的情況是放在了i18n目錄下,所以我們要去配置這個messages的路徑;

spring.messages.basename=i18n.login

配置頁面國際化值

去頁面獲取國際化的值,檢視Thymeleaf的文件,找到message取值操作為:#{...}。我們去頁面測試下:

IDEA還有提示,非常智慧的!

我們可以去啟動專案,訪問一下,發現已經自動識別為中文的了!

我們可以去啟動專案,訪問一下,發現已經自動識別為中文的了!

但是我們想要更好!可以根據按鈕自動切換中文英文!

配置國際化解析

在Spring中有一個國際化的Locale (區域資訊物件);裡面有一個叫做LocaleResolver (獲取區域資訊物件)的解析器!

我們去我們webmvc自動配置檔案,尋找一下!看到SpringBoot預設配置:

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
    // 容器中沒有就自己配,有的話就用使用者配置的
    if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    }
    // 接收頭國際化分解
    AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
    localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
    return localeResolver;
}

AcceptHeaderLocaleResolver 這個類中有一個方法

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // 預設的就是根據請求頭帶來的區域資訊獲取Locale進行國際化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    } else {
        Locale requestLocale = request.getLocale();
        List<Locale> supportedLocales = this.getSupportedLocales();
        if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
            Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
            if (supportedLocale != null) {
                return supportedLocale;
            } else {
                return defaultLocale != null ? defaultLocale : requestLocale;
            }
        } else {
            return requestLocale;
        }
    }
}

那假如我們現在想點選連結讓我們的國際化資源生效,就需要讓我們自己的Locale生效!

我們去自己寫一個自己的LocaleResolver,可以在連結上攜帶區域資訊!

修改一下前端頁面的跳轉連線:

<!-- 這裡傳入引數不需要使用 ?使用 (key=value)-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

我們去寫一個處理的元件類!

//可以在連結上攜帶區域資訊
public class MyLocaleResolver implements LocaleResolver {

    //解析請求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {

        String language = request.getParameter("l");
        Locale locale = Locale.getDefault(); // 如果沒有獲取到就使用系統預設的
        //如果請求連結不為空
        if (!StringUtils.isEmpty(language)){
            //分割請求引數
            String[] split = language.split("_");
            //國家,地區
            locale = new Locale(split[0],split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

為了讓我們的區域化資訊能夠生效,我們需要再配置一下這個元件!在我們自己的MvcConofig下新增bean;

@Bean
public LocaleResolver localeResolver(){
    return new MyLocaleResolver();
}

我們重啟專案,來訪問一下,發現點選按鈕可以實現成功切換!搞定收工!

10.3、登入功能及登入攔截器實現

  1. 登入頁表單提交路徑設定

    <form class="form-signin" th:action="@{/user/login}">
    
  2. 新建一個Controller來實現控制頁面的跳轉

    @Controller
    public class LoginController {
    
        @RequestMapping("/user/login")
        public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session){
    
            //具體的業務
            if (!StringUtils.isEmpty(username) && "123456".equals(password)){
                session.setAttribute("loginUser",username);
                return "redirect:/main.html";
            }else {
                //告訴使用者,你登陸失敗了
                model.addAttribute("msg","使用者名稱或密碼錯誤");
                return "index";
            }
    
        }
    }
    
  3. 使用三元運算子,控制msg內容是否在登入頁中顯示

    <!--/*@thymesVar id="msg" type="String"*/-->
    <!--如果msg的值為空,才顯示msg的訊息,可以不加判斷-->
    <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
    
  4. 新建一個攔截器LoginHandlerInterceptor實現HandlerInterceptor介面,在Controller類中已經給session賦值session.setAttribute("loginUser",username);

    public class LoginHandlerInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //登陸成功之後,應該有使用者的session;
    
            Object loginUser = request.getSession().getAttribute("loginUser");
    
            if (loginUser==null){ //沒有登入
                request.setAttribute("msg","沒有許可權,請先登入");
                request.getRequestDispatcher("/index.html").forward(request,response);
                return false;
            }else {
                return true;
            }
        }
    }
    
  5. 在MyMvcConfig類中重寫方法,以實現將新建的攔截器載入到配置中

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        //設定主頁訪問域名
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    
        //自定義的國際化元件就生效了!
        @Bean
        public LocaleResolver localeResolver(){
            return new MyLocaleResolver();
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/","/index.html","/user/login","/css/**","/img/**","/js/**");
        }
    }
    
  6. 將登入的使用者名稱載入到主頁

    <a class="navbar-brand col-sm-3 col-md-2 mr-0" th:href="@{http://getbootstrap.com/docs/4.0/examples/dashboard/#}">[[${session.loginUser}]]</a>
    

10.3、展示員工列表

  1. 提取頁面公共部分,將頭部導航欄及側邊欄程式碼提取到commons.html中;加入側邊欄a標籤判斷?:,通過其他頁面傳入值,來判斷是否點亮標籤

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
        <!--頭部導航欄-->
        <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
            <a class="navbar-brand col-sm-3 col-md-2 mr-0" th:href="@{http://getbootstrap.com/docs/4.0/examples/dashboard/#}">[[${session.loginUser}]]</a>
            <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
            <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                    <a class="nav-link" th:href="@{http://getbootstrap.com/docs/4.0/examples/dashboard/#}">登出</a>
                </li>
            </ul>
        </nav>
    
        <!--側邊欄-->
        <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
            <div class="sidebar-sticky">
                <ul class="nav flex-column">
                    <li class="nav-item">
                        <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/index.html}">
                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                                <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                                <polyline points="9 22 9 12 15 12 15 22"></polyline>
                            </svg>
                            首頁 <span class="sr-only">(current)</span>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                                <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                                <circle cx="9" cy="7" r="4"></circle>
                                <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                                <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                            </svg>
                            員工管理
                        </a>
                    </li>
                </ul>
            </div>
        </nav>
        
    </html>
    
  2. 主頁連結到EmployeeController,完成業務操作後攜帶查詢資料返回list.html頁面

    <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
            <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
            <circle cx="9" cy="7" r="4"></circle>
            <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
            <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
        </svg>
        員工管理
    </a>
    
    @Controller
    public class EmployeeController {
    
        @Autowired
        EmployeeDao employeeDao;
    
        @RequestMapping("/emps")
        public String list(Model model){
            Collection<Employee> employees = employeeDao.getAll();
            model.addAttribute("emps",employees);
            return "emp/list";
        }
    }
    
  3. list.html頁面同樣引入頭部導航欄與標題欄,並將查詢資訊遍歷輸出

    <!--引入頭部導航欄-->
    <div th:insert="~{commons/commons.html::topbar}"></div>
    
    <!--引入側邊欄-->
    <!--傳遞引數給側邊欄元件,使當前標籤高亮-->
    <div th:insert="~{commons/commons.html::sidebar(active='list.html')}"></div>
    
    <table class="table table-striped table-sm">
        <thead>
            <tr>
                <th>id</th>
                <th>lastName</th>
                <th>email</th>
                <th>gender</th>
                <th>department</th>
                <th>birth</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="emp:${emps}">
                <td th:text="${emp.getId()}"></td>
                <td th:text="${emp.getLastName()}"></td>
                <td th:text="${emp.getEmail()}"></td>
                <td th:text="${emp.getGender()==0?'女':'男'}"></td>
                <td th:text="${emp.getDepartment().getDepartmentName()}"></td>
                <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
                <td>
                    <button class="btn btn-sm btn-primary">編輯</button>
                    <button class="btn btn-sm btn-danger">刪除</button>
                </td>
            </tr>
        </tbody>
    </table>
    

10.4、增加員工實現

  1. 在list.xml頁面中指定位置新增增加使用者的a標籤,連結到controller(a標籤連結都以get方法提交)

    <a class="btn-sm btn-success" th:href="@{/emp}">新增員工</a>
    
  2. 在controller中新增對應的方法,並返回新增頁面(add.xml)

    @GetMapping("/emp") //指定get方法,可實現連結使用不同方法複用
    public String toAddpage(Model model){
        //查出所有部門的資訊
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);
    
        return "emp/add";
    }
    
  3. 新建add.xml頁面,並使用新增按鈕,再返回controller中的新增方法

    <!--引入頭部導航欄-->
    <div th:insert="~{commons/commons::topbar}"></div>
    
    <div class="container-fluid">
    	<div class="row">
    
    		<!--引入側邊欄-->
    		<div th:insert="~{commons/commons::sidebar}"></div>
    
    			<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    
    				<form class="form-control" th:action="@{/emp}" method="post">
    
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon1">LastName</span>
    						<input type="text" name="lastName" class="form-control" placeholder="kuangshen" aria-describedby="basic-addon1">
    					</div>
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon2">Email</span>
    						<input type="text" name="email" class="form-control" placeholder="24736743@qq.com" aria-describedby="basic-addon2">
    					</div>
    
    					<div class="form-group">
    						<p>Gender<br>
    							<input type="radio" name="gender" value="1">男
    							<input type="radio" name="gender" value="0">女
    						</p>
    					</div>
    
    					<div class="form-group">
    						<p>Department<br>
    							<!--我們在controller接收的是一個Employee,所以我們需要提交的是其中的一個屬性!-->
    							<!--需要傳遞物件的id,可以自動查詢出對應的物件,不能直接傳遞物件-->
    							<select name="department.id">
    								<option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
    							</select>
    						</p>
    					</div>
    
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon3">Birth</span>
    						<input type="text" name="birth" class="form-control" placeholder="2000-01-01" aria-describedby="basic-addon3">
    					</div>
    
    					<div class="form-group">
    						<button type="submit" class="btn btn-sm btn-success">新增</button>
    					</div>
    
    				</form>
    			</main>
    	</div>
    </div>
    
  4. 在controller中新建對應的新增方法,實現新增操作並返回list.xml

    @PostMapping("/emp")
    public String addEmp(Employee employee){
    
        employeeDao.save(employee); //呼叫底層業務方法儲存員工資訊
    
        //新增的操作 forward
        return "redirect:/emps";
    }
    

10.5、修改員工資訊的實現

  1. 在員工列表每個員工後新增編輯按鈕,並傳遞當前使用者id,連結到controller中的編輯方法

    <!--一定要完整拼接傳入引數與URL-->
    <a class="btn btn-sm btn-primary" th:href="@{/update/}+${employee.getId()}">編輯</a>
    
  2. 在編輯方法中查出該員工詳細資訊,並返回給編輯前端頁面

    //去員工的修改頁面
    //獲取位址列傳入引數,必須完整拼接url,並使用{}來獲取引數,使用@PathVariable獲取該引數
    @GetMapping("/update/{id}")
    public String toUpdateEmp(@PathVariable("id") int Id, Model model){
    
        //查出原來的資料
        Employee employee = employeeDao.getEmployeeById(Id);
        model.addAttribute("employee",employee);
        //查出所有部門的資訊
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);
    
        return "emp/update";
    }
    
  3. 在編輯的前端頁面中獲取原使用者資訊並修改使用者資訊,將修改後的使用者資訊再傳給後端

    <!--引入頭部導航欄-->
    <div th:insert="~{commons/commons::topbar}"></div>
    
    <div class="container-fluid">
    	<div class="row">
    
    		<!--引入側邊欄-->
    		<div th:insert="~{commons/commons::sidebar}"></div>
    
    			<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    
    				<form class="form-control" th:action="@{/updateEmp}" method="post">
    
    					<input type="hidden" name="id" th:value="${employee.getId()}">
    
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon1">LastName</span>
    						<input type="text" name="lastName" class="form-control" th:value="${employee.getLastName()}" aria-describedby="basic-addon1">
    					</div>
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon2">Email</span>
    						<input type="text" name="email" class="form-control" th:value="${employee.getEmail()}" aria-describedby="basic-addon2">
    					</div>
    
    					<div class="form-group">
    						<p>Gender<br>
    							<input th:checked="${employee.getGender()==1}" type="radio" name="gender" value="1">男
    							<input th:checked="${employee.getGender()==0}" type="radio" name="gender" value="0">女
    						</p>
    					</div>
    
    					<div class="form-group">
    						<p>Department<br>
    								<!--我們在controller接收的是一個Employee,所以我們需要提交的是其中的一個屬性!-->
    								<!--需要傳遞物件的id,可以自動查詢出對應的物件,不能直接傳遞物件-->
    							<select name="department.id">
    								<option th:selected="${dept.getId()==employee.getDepartment().getId()}"
    											th:each="dept:${departments}"
    											th:text="${dept.getDepartmentName()}"
    											th:value="${dept.getId()}"></option>
    							</select>
    						</p>
    					</div>
    
    					<div class="form-group">
    						<span class="input-group-addon" id="basic-addon3">Birth</span>
    						<input type="text" name="birth" class="form-control" th:value="${#dates.format(employee.getBirth(),'yyyy-MM-dd HH:mm:ss')}" aria-describedby="basic-addon3">
    					</div>
    
    					<div class="form-group">
    						<button type="submit" class="btn btn-sm btn-success">儲存</button>
    					</div>
    
    				</form>
    			</main>
    	</div>
    </div>
    
  4. 在controller方法中將使用者資訊儲存並返回list.xml頁面

    @PostMapping("/updateEmp")
    public String updateEmp(Employee employee){
        employeeDao.save(employee);
        return "redirect:/emps";
    }
    

10.6、刪除員工及404頁面

  1. 在list.xml頁面中,每個使用者後的刪除按鈕,傳送當前使用者id給controller方法,並轉到controller方法中

    <a class="btn btn-sm btn-danger" th:href="@{/delete/}+${employee.getId()}">刪除</a>
    
  2. 在controller方法中接收當前使用者id並執行刪除操作,返回list.xml頁面

    //刪除員工
    @GetMapping("/delete/{id}")
    public String deleteEmp(@PathVariable("id") Integer id){
    
        employeeDao.delete(id);
        return "redirect:/emps";
    }
    

設定指定錯誤頁面:可以設定程式錯誤時轉到指定的頁面,只需在templates資料夾下新建error資料夾,再將指定的錯誤頁面命名為指定錯誤型別,例如“404.html”,就會在出現該錯誤時自動轉到指定頁面

10.7、整合JDBC

SpringData簡介:

​ 對於資料訪問層,無論是 SQL(關係型資料庫) 還是 NOSQL(非關係型資料庫),Spring Boot 底層都是採用 Spring Data 的方式進行統一處理。Spring Boot 底層都是採用 Spring Data 的方式進行統一處理各種資料庫,Spring Data 也是 Spring 中與 Spring Boot、Spring Cloud 等齊名的知名專案。

整合JDBC:

建立測試專案測試資料來源

  1. 新建一個專案測試:springboot-data-jdbc ; 引入web模組、JDBC API模組和MySQL Driver模組!基礎模組

  2. 專案建好後,發現自動幫我們匯入瞭如下的啟動器

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
  3. 編寫yaml配置檔案連線資料庫

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
        driver-class-name: com.mysql.cj.jdbc.Driver
    
  4. 配置完這一些東西后,我們就可以直接去使用了,因為SpringBoot已經預設幫我們進行了自動配置;去測試類測試一下

    @SpringBootTest
    class SpringbootDataJdbcApplicationTests {
    
        //DI注入資料來源
        @Autowired
        DataSource dataSource;
    
        @Test
        public void contextLoads() throws SQLException {
            //看一下預設資料來源
            System.out.println(dataSource.getClass());
            //獲得連線
            Connection connection =   dataSource.getConnection();
            System.out.println(connection);
            //關閉連線
            connection.close();
        }
    }
    

    結果:我們可以看到他預設給我們配置的資料來源為 : class com.zaxxer.hikari.HikariDataSource , 我們並沒有手動配置。我們來全域性搜尋一下,找到資料來源的所有自動配置都在DataSourceAutoConfiguration檔案:

    @Import(
        {Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class}
    )
    protected static class PooledDataSourceConfiguration {
        protected PooledDataSourceConfiguration() {
        }
    }
    

    這裡匯入的類都在 DataSourceConfiguration 配置類下,可以看出 Spring Boot 2.2.5 預設使用HikariDataSource 資料來源,而以前版本,如 Spring Boot 1.5 預設使用 org.apache.tomcat.jdbc.pool.DataSource 作為資料來源;

    HikariDataSource 號稱 Java WEB 當前速度最快的資料來源,相比於傳統的 C3P0 、DBCP、Tomcat jdbc 等連線池更加優秀;

    可以使用 spring.datasource.type 指定自定義的資料來源型別,值為 要使用的連線池實現的完全限定名。

    關於資料來源我們並不做介紹,有了資料庫連線,顯然就可以 CRUD 運算元據庫了。但是我們需要先了解一個物件 JdbcTemplate

JDBCTemplate

  1. 有了資料來源(com.zaxxer.hikari.HikariDataSource),然後可以拿到資料庫連線(java.sql.Connection),有了連線,就可以使用原生的 JDBC 語句來運算元據庫;

  2. 即使不使用第三方第資料庫操作框架,如 MyBatis等,Spring 本身也對原生的JDBC 做了輕量級的封裝,即JdbcTemplate。

  3. 資料庫操作的所有 CRUD 方法都在 JdbcTemplate 中。

  4. Spring Boot 不僅提供了預設的資料來源,同時預設已經配置好了 JdbcTemplate 放在了容器中,程式設計師只需自己注入即可使用

  5. JdbcTemplate 的自動配置是依賴 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 類

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

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

測試

編寫一個Controller,注入 jdbcTemplate,編寫測試方法進行訪問測試;

@RestController
public class JDBCController {

    @Autowired
    JdbcTemplate jdbcTemplate;

    //查詢資料庫的所有資訊
    //沒有實體類,資料庫中的東西,怎麼獲取? Map
    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
        return list_maps;
    }

    @GetMapping("/addUser")
    public String addUser(){
        String sql = "insert into mybatis.user(id,name,pwd) values (5,'小明','123456')";
        jdbcTemplate.update(sql);
        return "addUser-ok";
    }

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id){
        String sql = "update mybatis.user set name=?,pwd=? where id="+id;

        //封裝
        Object[] objects = new Object[2];
        objects[0] = "小明2";
        objects[1] = "zzzzzz";
        jdbcTemplate.update(sql,objects);
        return "update-ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id){
        String sql = "delete from mybatis.user where id=?";
        jdbcTemplate.update(sql,id);
        return "delete-ok";
    }

}

測試請求,結果正常;

到此,CURD的基本操作,使用 JDBC 就搞定了。

10.8、整合Druid

整合Druid

Druid簡介

Java程式很大一部分要運算元據庫,為了提高效能運算元據庫的時候,又不得不使用資料庫連線池。

Druid 是阿里巴巴開源平臺上一個資料庫連線池實現,結合了 C3P0、DBCP 等 DB 池的優點,同時加入了日誌監控。

Druid 可以很好的監控 DB 池連線和 SQL 的執行情況,天生就是針對監控而生的 DB 連線池。

Druid已經在阿里巴巴部署了超過600個應用,經過一年多生產環境大規模部署的嚴苛考驗。

Spring Boot 2.0 以上預設使用 Hikari 資料來源,可以說 Hikari 與 Driud 都是當前 Java Web 上最優秀的資料來源,我們來重點介紹 Spring Boot 如何整合 Druid 資料來源,如何實現資料庫監控。

com.alibaba.druid.pool.DruidDataSource 基本配置引數如下:

配置資料來源

  1. 新增上Druid資料來源,Log4j的依賴

    <!--Druid-->
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.9</version>
    </dependency>
    <!--log4j-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  2. 切換資料來源;之前已經說過 Spring Boot 2.0 以上預設使用 com.zaxxer.hikari.HikariDataSource 資料來源,但可以 通過 spring.datasource.type 指定資料來源。設定資料來源連線初始化大小、最大連線數、等待時間、最小連線數 等設定項;可以檢視原始碼

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
        driver-class-name: com.mysql.cj.jdbc.Driver
        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注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  3. 現在需要程式設計師自己為 DruidDataSource 繫結全域性配置檔案中的引數,再新增到容器中,而不再使用 Spring Boot 的自動生成了;我們需要 自己新增 DruidDataSource 元件到容器中,並繫結屬性;

    @Configuration
    public class DruidConfig {
    
        /*
           將自定義的 Druid資料來源新增到容器中,不再讓 Spring Boot 自動建立
           繫結全域性配置檔案中的 druid 資料來源屬性到 com.alibaba.druid.pool.DruidDataSource從而讓它們生效
           @ConfigurationProperties(prefix = "spring.datasource"):作用就是將 全域性配置檔案中
           字首為 spring.datasource的屬性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名引數中
         */
        
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    }
    
  4. 去測試類中測試一下;看是否成功!

    @SpringBootTest
    class SpringbootDataJdbcApplicationTests {
    
        //DI注入資料來源
        @Autowired
        DataSource dataSource;
    
        @Test
        public void contextLoads() throws SQLException {
            //看一下預設資料來源
            System.out.println(dataSource.getClass());
            //獲得連線
            Connection connection =   dataSource.getConnection();
            System.out.println(connection);
    
            DruidDataSource druidDataSource = (DruidDataSource) dataSource;
            System.out.println("druidDataSource 資料來源最大連線數:" + druidDataSource.getMaxActive());
            System.out.println("druidDataSource 資料來源初始化連線數:" + druidDataSource.getInitialSize());
    
            //關閉連線
            connection.close();
        }
    }
    

    輸出結果:可見配置引數已經生效!

配置Druid資料來源監控

Druid 資料來源具有監控的功能,並提供了一個 web 介面方便使用者檢視,類似安裝 路由器 時,人家也提供了一個預設的 web 頁面。

所以第一步需要設定 Druid 的後臺管理頁面,比如 登入賬號、密碼 等;配置後臺管理;

//配置 Druid 監控管理後臺的Servlet;
//內建 Servlet 容器時沒有web.xml檔案,所以使用 Spring Boot 的註冊 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
    ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

    // 這些引數可以在 com.alibaba.druid.support.http.StatViewServlet 
    // 的父類 com.alibaba.druid.support.http.ResourceServlet 中找到
    Map<String, String> initParams = new HashMap<>();
    initParams.put("loginUsername", "admin"); //後臺管理介面的登入賬號
    initParams.put("loginPassword", "123456"); //後臺管理介面的登入密碼

    //後臺允許誰可以訪問
    //initParams.put("allow", "localhost"):表示只有本機可以訪問
    //initParams.put("allow", ""):為空或者為null時,表示允許所有訪問
    initParams.put("allow", "");
    //deny:Druid 後臺拒絕誰訪問
    //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip訪問

    //設定初始化引數
    bean.setInitParameters(initParams);
    return bean;
}

配置完畢後,我們可以選擇訪問 :http://localhost:8080/druid/login.html

配置 Druid web 監控 filter 過濾器

//配置 Druid 監控 之  web 監控的 filter
//WebStatFilter:用於配置Web和Druid資料來源之間的管理關聯監控統計
@Bean
public FilterRegistrationBean webStatFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());

    //exclusions:設定哪些請求進行過濾排除掉,從而不進行統計
    Map<String, String> initParams = new HashMap<>();
    initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
    bean.setInitParameters(initParams);

    //"/*" 表示過濾所有請求
    bean.setUrlPatterns(Arrays.asList("/*"));
    return bean;
}

平時在工作中,按需求進行配置即可,主要用作監控!

10.9、整合MyBatis

整合測試

  1. 匯入MyBatis所需要的依賴

    <!--mybatis-spring-boot-starter-->
    <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    
  2. 配置資料庫連線資訊(不變)

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
        driver-class-name: com.mysql.cj.jdbc.Driver
        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注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  3. 測試資料庫是否連線成功!

  4. 建立實體類,匯入Lombok!

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
    
        private int id;
        private String name;
        private String pwd;
    }
    
  5. 建立mapper目錄以及對應的Mapper介面

    //這個註解表示了這是一個mybatis的mapper類; Dao
    @Mapper
    @Repository
    public interface UserMapper {
    
        List<User> queryUserList();
    
        User queryUserById(int id);
    
        int addUser(User user);
    
        int deleteUser(int id);
    
        int updateUser(User user);
    
    }
    
  6. 對應的Mapper對映檔案(resources/mybatis/mapper/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.kuang.mapper.UserMapper">
    
        <select id="queryUserList" resultType="User">
            select * from mybatis.user
        </select>
    
        <select id="queryUserById" resultType="User">
            select * from mybatis.user where id=#{id}
        </select>
    
        <insert id="addUser" parameterType="User">
            insert into mybatis.user values name=#{name},pwd=#{pwd}
        </insert>
    
        <delete id="deleteUser">
            delete from mybatis.user where id=#{id}
        </delete>
    
        <update id="updateUser" parameterType="User">
            update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id}
        </update>
    </mapper>
    
  7. maven配置資源過濾問題

    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
    
  8. 編寫UserController進行測試!

    @RestController
    public class UserController {
    
        @Autowired
        private UserMapper userMapper;
    
        @GetMapping("/queryUserList")
        public List<User> queryUserList(){
            List<User> userList = userMapper.queryUserList();
            for (User user : userList) {
                System.out.println(user);
            }
            return userList;
        }
        @GetMapping("/queryUserById")
        public User queryUserById(){
            User user = userMapper.queryUserById(2);
            System.out.println(user);
            return user;
        }
        @GetMapping("/addUser")
        public int addUser(){
            int i = userMapper.addUser(new User(5, "qinjiang", "123456"));
            return i;
        }
        @GetMapping("/updateUser")
        public int updateUser(){
            int i = userMapper.updateUser(new User(4, "kuangshne", "123456"));
            return i;
        }
        @GetMapping("/deleteUser")
        public int deleteUser(){
            int i = userMapper.deleteUser(5);
            return i;
        }
    }
    

    啟動專案訪問進行測試!

我們增加一個員工類再測試下,為之後做準備

  1. 新建一個pojo類Employee

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Employee {
    
        private Integer id;
        private String lastName;
        private String email;
        //1 male, 0 female
        private Integer gender;
        private Integer department;
        private Date birth;
    
    }
    
  2. 新建一個 EmployeeMapper 介面

    //@Mapper : 表示本類是一個 MyBatis 的 Mapper
    @Mapper
    @Repository
    public interface EmployeeMapper {
    
        // 獲取所有員工資訊
        List<Employee> getEmployees();
    
        // 新增一個員工
        int save(Employee employee);
    
        // 通過id獲得員工資訊
        Employee get(Integer id);
    
        // 通過id刪除員工
        int delete(Integer id);
    
    }
    
  3. 編寫EmployeeMapper.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.kuang.mapper.EmployeeMapper">
    
        <resultMap id="EmployeeMap" type="Employee">
            <id property="id" column="eid"/>
            <result property="lastName" column="last_name"/>
            <result property="email" column="email"/>
            <result property="gender" column="gender"/>
            <result property="birth" column="birth"/>
            <association property="eDepartment"  javaType="Department">
                <id property="id" column="did"/>
                <result property="departmentName" column="dname"/>
            </association>
        </resultMap>
    
        <select id="getEmployees" resultMap="EmployeeMap">
            select e.id as eid,last_name,email,gender,birth,d.id as did,d.department_name as dname
            from department d,employee e
            where d.id = e.department
        </select>
    
        <insert id="save" parameterType="Employee">
            insert into employee (last_name,email,gender,department,birth)
            values (#{lastName},#{email},#{gender},#{department},#{birth});
        </insert>
    
        <select id="get" resultType="Employee">
            select * from employee where id = #{id}
        </select>
    
        <delete id="delete" parameterType="int">
            delete from employee where id = #{id}
        </delete>
    
    </mapper>
    
  4. 編寫EmployeeController類進行測試

    @RestController
    public class EmployeeController {
    
        @Autowired
        EmployeeMapper employeeMapper;
    
        // 獲取所有員工資訊
        @GetMapping("/getEmployees")
        public List<Employee> getEmployees(){
            return employeeMapper.getEmployees();
        }
    
        @GetMapping("/save")
        public int save(){
            Employee employee = new Employee();
            employee.setLastName("kuangshen");
            employee.setEmail("qinjiang@qq.com");
            employee.setGender(1);
            employee.setDepartment(101);
            employee.setBirth(new Date());
            return employeeMapper.save(employee);
        }
    
        // 通過id獲得員工資訊
        @GetMapping("/get/{id}")
        public Employee get(@PathVariable("id") Integer id){
            return employeeMapper.get(id);
        }
    
        // 通過id刪除員工
        @GetMapping("/delete/{id}")
        public int delete(@PathVariable("id") Integer id){
            return employeeMapper.delete(id);
        }
    
    }
    

    測試結果完成,搞定收工!

11、SpringSecurity

安全簡介

在 Web 開發中,安全一直是非常重要的一個方面。安全雖然屬於應用的非功能性需求,但是應該在應用開發的初期就考慮進來。如果在應用開發的後期才考慮安全的問題,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,無法滿足使用者的要求,並可能造成使用者的隱私資料被攻擊者竊取;另一方面,應用的基本架構已經確定,要修復安全漏洞,可能需要對系統的架構做出比較重大的調整,因而需要更多的開發時間,影響應用的釋出程式。因此,從應用開發的第一天就應該把安全相關的因素考慮進來,並在整個應用的開發過程中。

市面上存在比較有名的:Shiro,Spring Security !

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它實際上是保護基於spring的應用程式的標準。

Spring Security是一個框架,側重於為Java應用程式提供身份驗證和授權。與所有Spring專案一樣,Spring安全性的真正強大之處在於它可以輕鬆地擴充套件以滿足定製需求

從官網的介紹中可以知道這是一個許可權框架。想我們之前做專案是沒有使用框架是怎麼控制許可權的?對於許可權 一般會細分為功能許可權,訪問許可權,和選單許可權。程式碼會寫的非常的繁瑣,冗餘。

怎麼解決之前寫許可權程式碼繁瑣,冗餘的問題,一些主流框架就應運而生而Spring Scecurity就是其中的一種。

Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括使用者認證(Authentication)和使用者授權(Authorization)兩個部分。使用者認證指的是驗證某個使用者是否為系統中的合法主體,也就是說使用者能否訪問該系統。使用者認證一般要求使用者提供使用者名稱和密碼。系統通過校驗使用者名稱和密碼來完成認證過程。使用者授權指的是驗證某個使用者是否有許可權執行某個操作。在一個系統中,不同使用者所具有的許可權是不同的。比如對一個檔案來說,有的使用者只能進行讀取,而有的使用者可以進行修改。一般來說,系統會為不同的使用者分配不同的角色,而每個角色則對應一系列的許可權。

對於上面提到的兩種應用情景,Spring Security 框架都有很好的支援。在使用者認證方面,Spring Security 框架支援主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。在使用者授權方面,Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List,ACL),可以對應用中的領域物件進行細粒度的控制。

實戰測試

實驗環境搭建

  1. 新建一個初始的springboot專案web模組,thymeleaf模組

  2. 匯入靜態資源

    welcome.html
    |views
    |level1
    1.html
    2.html
    3.html
    |level2
    1.html
    2.html
    3.html
    |level3
    1.html
    2.html
    3.html
    Login.html
    
  3. controller跳轉!

    @Controller
    public class RouterController {
    
        @RequestMapping({"/","/index"})
        public String index(){
            return "index";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin(){
            return "views/login";
        }
    
        @RequestMapping("/level1/{id}")
        public String level1(@PathVariable("id") int id){
            return "views/level1/"+id;
        }
    
        @RequestMapping("/level2/{id}")
        public String level2(@PathVariable("id") int id){
            return "views/level2/"+id;
        }
    
        @RequestMapping("/level3/{id}")
        public String level3(@PathVariable("id") int id){
            return "views/level3/"+id;
        }
    
    }
    
  4. 測試實驗環境是否OK!

認識SpringSecurity

Spring Security 是針對Spring專案的安全框架,也是Spring Boot底層安全模組預設的技術選型,他可以實現強大的Web安全控制,對於安全控制,我們僅需要引入 spring-boot-starter-security 模組,進行少量的配置,即可實現強大的安全管理!

記住幾個類:

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

Spring Security的兩個主要目標是 “認證” 和 “授權”(訪問控制)。

“認證”(Authentication)

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

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

“授權” (Authorization)

授權發生在系統成功驗證您的身份後,最終會授予您訪問資源(如資訊,檔案,資料庫,資金,位置,幾乎任何內容)的完全許可權。

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

認證和授權

目前,我們的測試環境,是誰都可以訪問的,我們使用 Spring Security 增加上認證和授權的功能

  1. 引入Srping Security模組

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 編寫Spring Security配置類

    參考官網:https://spring.io/projects/spring-security

    檢視我們自己專案中的版本,找到對應的幫助文件:

    https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4

  3. 編寫基礎配置類

    @EnableWebSecurity // 開啟WebSecurity模式
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
        }
    }
    
  4. 定製請求的授權規則

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 定製請求的授權規則
        // 首頁所有人可以訪問
        http.authorizeRequests().antMatchers("/").permitAll()
            .antMatchers("/level1/**").hasRole("vip1")
            .antMatchers("/level2/**").hasRole("vip2")
            .antMatchers("/level3/**").hasRole("vip3");
    }
    
  5. 測試一下:發現除了首頁都進不去了,因為我們目前沒有登入的角色,因為請求需要登入的角色擁有對應的許可權才可以!

  6. 在SecurityConfig()方法中加入以下配置,開啟自動配置的登入功能!

    // 開啟自動配置的登入功能
    // /login 請求來到登入頁
    // /login?error 重定向到這裡表示登入失敗
    http.formLogin();
    
  7. 測試一下:發現,沒有許可權的時候,會跳轉到登入的頁面!

  8. 檢視剛才登入頁的註釋資訊;我們可以定義認證規則,重寫configure(AuthenticationManagerBuilder auth)方法

    //定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       
       //在記憶體中定義,也可以在jdbc中去拿....
       auth.inMemoryAuthentication()
              .withUser("kuangshen").password("123456").roles("vip2","vip3")
              .and()
              .withUser("root").password("123456").roles("vip1","vip2","vip3")
              .and()
              .withUser("guest").password("123456").roles("vip1","vip2");
    }
    
  9. 測試,我們可以使用這些賬號登入進行測試!發現會報錯!There is no PasswordEncoder mapped for the id “null”

  10. 原因,我們要將前端傳過來的密碼進行某種方式加密,否則就無法登入,修改程式碼

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
        //這些資料正常應該從資料庫中讀
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }
    
  11. 測試,發現登入成功,並且每個角色只能訪問自己認證下的規則!搞定

許可權控制和登出

  1. 開啟自動配置的登出的功能

    //定製請求的授權規則
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //....
        //開啟自動配置的登出的功能
        // /logout 登出請求
        http.logout();
    }
    
  2. 我們在前端,增加一個登出的按鈕,index.html導航欄中

    <a class="item" th:href="@{/logout}">
        <i class="address card icon"></i> 登出
    </a>
    
  3. 我們可以去測試一下,登入成功後點選登出,發現登出完畢會跳轉到登入頁面!

  4. 但是,我們想讓它登出成功後,依舊可以跳轉到首頁,該怎麼處理呢?

    // .logoutSuccessUrl("/"); 登出成功來到首頁
    http.logout().logoutSuccessUrl("/");
    
  5. 測試,登出完畢後,發現跳轉到首頁OK

  6. 我們現在又來一個需求:使用者沒有登入的時候,導航欄上只顯示登入按鈕,使用者登入之後,導航欄可以顯示登入的使用者資訊及登出按鈕!還有就是,比如kuangshen這個使用者,它只有 vip2,vip3功能,那麼登入則只顯示這兩個功能,而vip1的功能選單不顯示!這個就是真實的網站情況了!該如何做呢?

    我們需要結合thymeleaf中的一些功能

    sec:authorize="isAuthenticated()":是否認證登入!來顯示不同的頁面

    Maven依賴:

    <!--security-thymeleaf整合包-->
    <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    
  7. 修改我們的前端頁面,匯入名稱空間

    xmlns:sec="http://www.thymeleaf.org/extra/spring-security"
    

    修改導航欄,增加認證判斷

    <!--登入登出-->
    <div class="right menu">
    
       <!--如果未登入-->
       <div sec:authorize="!isAuthenticated()">
           <a class="item" th:href="@{/login}">
               <i class="address card icon"></i> 登入
           </a>
       </div>
    
       <!--如果已登入-->
       <div sec:authorize="isAuthenticated()">
           <a class="item">
               <i class="address card icon"></i>
              使用者名稱:<span sec:authentication="principal.username"></span>
              角色:<span sec:authentication="principal.authorities"></span>
           </a>
       </div>
    
       <div sec:authorize="isAuthenticated()">
           <a class="item" th:href="@{/logout}">
               <i class="address card icon"></i> 登出
           </a>
       </div>
    </div>
    
  8. 重啟測試,我們可以登入試試看,登入成功後確實顯示了我們想要的頁面;

  9. 如果登出404了,就是因為它預設防止csrf跨站請求偽造,因為會產生安全問題,我們可以將請求改為post表單提交,或者在spring security中關閉csrf功能;我們試試:在 配置中增加http.csrf().disable();

    http.csrf().disable();//關閉csrf功能:跨站請求偽造,預設只能通過post方式提交logout請求
    http.logout().logoutSuccessUrl("/");
    
  10. 我們繼續將下面的角色功能塊認證完成!

    <!-- sec:authorize="hasRole('vip1')" -->
    <div class="column" sec:authorize="hasRole('vip1')">
       <div class="ui raised segment">
           <div class="ui">
               <div class="content">
                   <h5 class="content">Level 1</h5>
                   <hr>
                   <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                   <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                   <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
               </div>
           </div>
       </div>
    </div>
    
    <div class="column" sec:authorize="hasRole('vip2')">
       <div class="ui raised segment">
           <div class="ui">
               <div class="content">
                   <h5 class="content">Level 2</h5>
                   <hr>
                   <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                   <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                   <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
               </div>
           </div>
       </div>
    </div>
    
    <div class="column" sec:authorize="hasRole('vip3')">
       <div class="ui raised segment">
           <div class="ui">
               <div class="content">
                   <h5 class="content">Level 3</h5>
                   <hr>
                   <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                   <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                   <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
               </div>
           </div>
       </div>
    </div>
    
  11. 測試一下

  12. 許可權控制和登出搞定!

記住我

現在的情況,我們只要登入之後,關閉瀏覽器,再登入,就會讓我們重新登入,但是很多網站的情況,就是有一個記住密碼的功能,這個該如何實現呢?很簡單

  1. 開啟記住我功能

    //定製請求的授權規則
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //。。。。。。。。。。。
        //記住我
        http.rememberMe();
    }
    
  2. 我們再次啟動專案測試一下,發現登入頁多了一個記住我功能,我們登入之後關閉 瀏覽器,然後重新開啟瀏覽器訪問,發現使用者依舊存在!

    思考:如何實現的呢?其實非常簡單

    我們可以檢視瀏覽器的cookie

  3. 我們點選登出的時候,可以發現,spring security 幫我們自動刪除了這個 cookie

  4. 結論:登入成功後,將cookie傳送給瀏覽器儲存,以後登入帶上這個cookie,只要通過檢查就可以免登入了。如果點選登出,則會刪除這個cookie,具體的原理我們在JavaWeb階段都講過了,這裡就不在多說了!

定製登入頁

現在這個登入頁面都是spring security 預設的,怎麼樣可以使用我們自己寫的Login介面呢?

  1. 在剛才的登入頁配置後面指定 loginpage

    http.formLogin().loginPage("/toLogin");
    
  2. 然後前端也需要指向我們自己定義的 login請求

    <a class="item" th:href="@{/toLogin}">
        <i class="address card icon"></i> 登入
    </a>
    
  3. 我們登入,需要將這些資訊傳送到哪裡,我們也需要配置,login.html配置提交請求及方式,方式必須為post,在loginPage()原始碼的註釋上有寫明

    <form th:action="@{/login}" method="post">
       <div class="field">
           <label>Username</label>
           <div class="ui left icon input">
               <input type="text" placeholder="Username" name="username">
               <i class="user icon"></i>
           </div>
       </div>
       <div class="field">
           <label>Password</label>
           <div class="ui left icon input">
               <input type="password" name="password">
               <i class="lock icon"></i>
           </div>
       </div>
       <input type="submit" class="ui blue submit button"/>
    </form>
    
  4. 這個請求提交上來,我們還需要驗證處理,怎麼做呢?我們可以檢視formLogin()方法的原始碼!我們配置接受登入的使用者名稱和密碼的引數!

    http.formLogin()
      .usernameParameter("username")
      .passwordParameter("password")
      .loginPage("/toLogin")
      .loginProcessingUrl("/login"); // 登陸表單提交請求
    
  5. 在登入頁增加記住我的多選框

    <input type="checkbox" name="remember"> 記住我
    
  6. 後端驗證處理!

    //定製記住我的引數!
    http.rememberMe().rememberMeParameter("remember");
    
  7. 測試,OK

完整配置程式碼

@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");


        //開啟自動配置的登入功能:如果沒有許可權,就會跳轉到登入頁面!
        // /login 請求來到登入頁
        // /login?error 重定向到這裡表示登入失敗
        http.formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLogin")
            .loginProcessingUrl("/login"); // 登陸表單提交請求

        //開啟自動配置的登出的功能
        // /logout 登出請求
        // .logoutSuccessUrl("/"); 登出成功來到首頁

        http.csrf().disable();//關閉csrf功能:跨站請求偽造,預設只能通過post方式提交logout請求
        http.logout().logoutSuccessUrl("/");

        //記住我
        http.rememberMe().rememberMeParameter("remember");
    }

    //定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在記憶體中定義,也可以在jdbc中去拿....
        //Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。
        //要想我們的專案還能夠正常登陸,需要修改一下configure中的程式碼。我們要將前端傳過來的密碼進行某種方式加密
        //spring security 官方推薦的是使用bcrypt加密方式。

        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
    }
}

12、專案整合Swagger

學習目標:

  • 瞭解Swagger的概念及作用
  • 掌握在專案中整合Swagger自動生成API文件

Swagger簡介

前後端分離

  • 前端 -> 前端控制層、檢視層
  • 後端 -> 後端控制層、服務層、資料訪問層
  • 前後端通過API進行互動
  • 前後端相對獨立且鬆耦合

產生的問題

  • 前後端整合,前端或者後端無法做到“及時協商,儘早解決”,最終導致問題集中爆發

解決方案

  • 首先定義schema [ 計劃的提綱 ],並實時跟蹤最新的API,降低整合風險

Swagger

  • 號稱世界上最流行的API框架
  • Restful Api 文件線上自動生成器 => API 文件 與API 定義同步更新
  • 直接執行,線上測試API
  • 支援多種語言 (如:Java,PHP等)
  • 官網:https://swagger.io/

SpringBoot整合Swagger

SpringBoot整合Swagger => springfox,兩個jar包

  • Springfox-swagger2
  • swagger-springmvc

使用Swagger

要求:jdk 1.8 + 否則swagger2無法執行

步驟:

  1. 新建一個SpringBoot-web專案

  2. 新增Maven依賴

    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  3. 編寫HelloController,測試確保執行成功!

  4. 要使用Swagger,我們需要編寫一個配置類-SwaggerConfig來配置 Swagger

    @Configuration //配置類
    @EnableSwagger2// 開啟Swagger2的自動配置
    public class SwaggerConfig {  
    }
    
  5. 訪問測試 :http://localhost:8080/swagger-ui.html ,可以看到swagger的介面;

配置Swagger

  1. Swagger例項Bean是Docket,所以通過配置Docket例項來配置Swaggger。

    @Bean //配置docket以配置Swagger具體引數
    public Docket docket() {
       return new Docket(DocumentationType.SWAGGER_2);
    }
    
  2. 可以通過apiInfo()屬性配置文件資訊

    //配置文件資訊
    private ApiInfo apiInfo() {
        Contact contact = new Contact("聯絡人名字", "http://xxx.xxx.com/聯絡人訪問連結", "聯絡人郵箱");
        return new ApiInfo(
            "Swagger學習", // 標題
            "學習演示如何配置Swagger", // 描述
            "v1.0", // 版本
            "http://terms.service.url/組織連結", // 組織連結
            contact, // 聯絡人資訊
            "Apach 2.0 許可", // 許可
            "許可連結", // 許可連線
            new ArrayList<>()// 擴充套件
        );
    }
    
  3. Docket 例項關聯上 apiInfo()

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
    }
    
  4. 重啟專案,訪問測試 http://localhost:8080/swagger-ui.html 看下效果;

配置掃描介面

  1. 構建Docket時通過select()方法配置怎麼掃描介面。

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()// 通過.select()方法,去配置掃描介面,RequestHandlerSelectors配置如何掃描介面
            .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
            .build();
    }
    
  2. 重啟專案測試,由於我們配置根據包的路徑掃描介面,所以我們只能看到一個類

  3. 除了通過包路徑配置掃描介面外,還可以通過配置其他方式掃描介面,這裡註釋一下所有的配置方式:

    any() // 掃描所有,專案中的所有介面都會被掃描到
    none() // 不掃描介面
    // 通過方法上的註解掃描,如withMethodAnnotation(GetMapping.class)只掃描get請求
    withMethodAnnotation(final Class<? extends Annotation> annotation)
    // 通過類上的註解掃描,如.withClassAnnotation(Controller.class)只掃描有controller註解的類中的介面
    withClassAnnotation(final Class<? extends Annotation> annotation)
    basePackage(final String basePackage) // 根據包路徑掃描介面
    
  4. 除此之外,我們還可以配置介面掃描過濾:

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()// 通過.select()方法,去配置掃描介面,RequestHandlerSelectors配置如何掃描介面
            .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
            // 配置如何通過path過濾,即這裡只掃描請求以/kuang開頭的介面
            .paths(PathSelectors.ant("/kuang/**"))
            .build();
    }
    
  5. 這裡的可選值還有

    any() // 任何請求都掃描
    none() // 任何請求都不掃描
    regex(final String pathRegex) // 通過正規表示式控制
    ant(final String antPattern) // 通過ant()控制
    

配置Swagger開關

  1. 通過enable()方法配置是否啟用swagger,如果是false,swagger將不能在瀏覽器中訪問了

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .enable(false) //配置是否啟用Swagger,如果是false,在瀏覽器將無法訪問
            .select()// 通過.select()方法,去配置掃描介面,RequestHandlerSelectors配置如何掃描介面
            .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
            // 配置如何通過path過濾,即這裡只掃描請求以/kuang開頭的介面
            .paths(PathSelectors.ant("/kuang/**"))
            .build();
    }
    
  2. 如何動態配置當專案處於test、dev環境時顯示swagger,處於prod時不顯示?

    @Bean
    public Docket docket(Environment environment) {
        // 設定要顯示swagger的環境
        Profiles of = Profiles.of("dev", "test");
        // 判斷當前是否處於該環境
        // 通過 enable() 接收此引數判斷是否要顯示
        boolean b = environment.acceptsProfiles(of);
    
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .enable(b) //配置是否啟用Swagger,如果是false,在瀏覽器將無法訪問
            .select()// 通過.select()方法,去配置掃描介面,RequestHandlerSelectors配置如何掃描介面
            .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
            // 配置如何通過path過濾,即這裡只掃描請求以/kuang開頭的介面
            .paths(PathSelectors.ant("/kuang/**"))
            .build();
    }
    
  3. 可以在專案中增加一個dev的配置檔案檢視效果!

配置API分組

  1. 如果沒有配置分組,預設是default。通過groupName()方法即可配置分組:

    @Bean
    public Docket docket(Environment environment) {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .groupName("hello") // 配置分組
            // 省略配置....
    }
    
  2. 重啟專案檢視分組

  3. 如何配置多個分組?配置多個分組只需要配置多個docket即可:

    @Bean
    public Docket docket1(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
    }
    @Bean
    public Docket docket2(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
    }
    @Bean
    public Docket docket3(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
    }
    
  4. 重啟專案檢視即可

實體配置

  1. 新建一個實體類

    @ApiModel("使用者實體")
    public class User {
        @ApiModelProperty("使用者名稱")
        public String username;
        @ApiModelProperty("密碼")
        public String password;
    }
    
  2. 只要這個實體在請求介面的返回值上(即使是泛型),都能對映到實體項中:

    @RequestMapping("/getUser")
    public User getUser(){
        return new User();
    }
    
  3. 重啟檢視測試

    注:並不是因為@ApiModel這個註解讓實體顯示在這裡了,而是隻要出現在介面方法的返回值上的實體都會顯示在這裡,而@ApiModel和@ApiModelProperty這兩個註解只是為實體新增註釋的。

    @ApiModel為類新增註釋

    @ApiModelProperty為類屬性新增註釋

常用註解

Swagger的所有註解定義在io.swagger.annotations包下

下面列一些經常用到的,未列舉出來的可以另行查閱說明:

Swagger註解 簡單說明
@Api(tags = "xxx模組說明") 作用在模組類上
@ApiOperation("xxx介面說明") 作用在介面方法上
@ApiModel("xxxPOJO說明") 作用在模型類上:如VO、BO
@ApiModelProperty(value = "xxx屬性說明",hidden = true) 作用在類方法和屬性上,hidden設定為true可以隱藏該屬性
@ApiParam("xxx引數說明") 作用在引數、方法和欄位上,類似@ApiModelProperty

我們也可以給請求的介面配置一些註釋

@ApiOperation("狂神的介面")
@PostMapping("/kuang")
@ResponseBody
public String kuang(@ApiParam("這個名字會被返回")String username){
    return username;
}

這樣的話,可以給一些比較難理解的屬性或者介面,增加一些配置資訊,讓人更容易閱讀!

相較於傳統的Postman或Curl方式測試介面,使用swagger簡直就是傻瓜式操作,不需要額外說明文件(寫得好本身就是文件)而且更不容易出錯,只需要錄入資料然後點選Execute,如果再配合自動化框架,可以說基本就不需要人為操作了。

Swagger是個優秀的工具,現在國內已經有很多的中小型網際網路公司都在使用它,相較於傳統的要先出Word介面文件再測試的方式,顯然這樣也更符合現在的快速迭代開發行情。當然了,提醒下大家在正式環境要記得關閉Swagger,一來出於安全考慮二來也可以節省執行時記憶體。

擴充:其他皮膚

我們可以匯入不同的包實現不同的皮膚定義:

  1. 預設的 訪問 http://localhost:8080/swagger-ui.html

    <dependency>
       <groupId>io.springfox</groupId>
       <artifactId>springfox-swagger-ui</artifactId>
       <version>2.9.2</version>
    </dependency>
    
  2. bootstrap-ui 訪問 http://localhost:8080/doc.html

    <!-- 引入swagger-bootstrap-ui包 /doc.html-->
    <dependency>
       <groupId>com.github.xiaoymin</groupId>
       <artifactId>swagger-bootstrap-ui</artifactId>
       <version>1.9.1</version>
    </dependency>
    
  3. Layui-ui 訪問 http://localhost:8080/docs.html

    <!-- 引入swagger-ui-layer包 /docs.html-->
    <dependency>
       <groupId>com.github.caspar-chen</groupId>
       <artifactId>swagger-ui-layer</artifactId>
       <version>1.1.3</version>
    </dependency>
    
  4. mg-ui 訪問 http://localhost:8080/document.html

    <!-- 引入swagger-ui-layer包 /document.html-->
    <dependency>
       <groupId>com.zyplayer</groupId>
       <artifactId>swagger-mg-ui</artifactId>
       <version>1.0.6</version>
    </dependency>
    

13、非同步、定時、郵件任務

前言:

在我們的工作中,常常會用到非同步處理任務,比如我們在網站上傳送郵件,後臺會去傳送郵件,此時前臺會造成響應不動,直到郵件傳送完畢,響應才會成功,所以我們一般會採用多執行緒的方式去處理這些任務。還有一些定時任務,比如需要在每天凌晨的時候,分析一次前一天的日誌資訊。

非同步任務

  1. 建立一個service包

  2. 建立一個類AsyncService

    非同步處理還是非常常用的,比如我們在網站上傳送郵件,後臺會去傳送郵件,此時前臺會造成響應不動,直到郵件傳送完畢,響應才會成功,所以我們一般會採用多執行緒的方式去處理這些任務。

    編寫方法,假裝正在處理資料,使用執行緒設定一些延時,模擬同步等待的情況;

    @Service
    public class AsyncService {
    
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("業務進行中....");
        }
    }
    
  3. 編寫controller包

  4. 編寫AsyncController類

    我們去寫一個Controller測試一下

@RestController
public class AsyncController {

   @Autowired
   AsyncService asyncService;

   @GetMapping("/hello")
   public String hello(){
       asyncService.hello();
       return "success";
  }
}
  1. 訪問http://localhost:8080/hello進行測試,3秒後出現success,這是同步等待的情況。

    問題:我們如果想讓使用者直接得到訊息,就在後臺使用多執行緒的方式進行處理即可,但是每次都需要自己手動去編寫多執行緒的實現的話,太麻煩了,我們只需要用一個簡單的辦法,在我們的方法上加一個簡單的註解即可,如下:

  2. 給hello方法新增@Async註解;

    //告訴Spring這是一個非同步方法
    @Async
    public void hello(){
       try {
           Thread.sleep(3000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("業務進行中....");
    }
    

    SpringBoot就會自己開一個執行緒池,進行呼叫!但是要讓這個註解生效,我們還需要在主程式上新增一個註解@EnableAsync ,開啟非同步註解功能;

    @EnableAsync //開啟非同步註解功能
    @SpringBootApplication
    public class SpringbootTaskApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootTaskApplication.class, args);
        }
    
    }
    
  3. 重啟測試,網頁瞬間響應,後臺程式碼依舊執行!

定時任務

專案開發中經常需要執行一些定時任務,比如需要在每天凌晨的時候,分析一次前一天的日誌資訊,Spring為我們提供了非同步執行任務排程的方式,提供了兩個介面。

  • TaskExecutor介面
  • TaskScheduler介面

兩個註解:

  • @EnableScheduling
  • @Scheduled

cron表示式:

測試步驟:

  1. 建立一個ScheduledService

    我們裡面存在一個hello方法,他需要定時執行,怎麼處理呢?

    @Service
    public class ScheduledService {
    
        //秒   分   時     日   月   周幾
        //0 * * * * MON-FRI
        //注意cron表示式的用法;
        @Scheduled(cron = "0 * * * * 0-7")
        public void hello(){
            System.out.println("hello.....");
        }
    }
    
  2. 這裡寫完定時任務之後,我們需要在主程式上增加@EnableScheduling開啟定時任務功能

    @EnableAsync //開啟非同步註解功能
    @EnableScheduling //開啟基於註解的定時任務
    @SpringBootApplication
    public class SpringbootTaskApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootTaskApplication.class, args);
        }
    
    }
    
  3. 我們來詳細瞭解下cron表示式;

    http://www.bejson.com/othertools/cron/

  4. 常用的表示式

    (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觸發
    

郵件任務

郵件傳送,在我們的日常開發中,也非常的多,Springboot也幫我們做了支援

  • 郵件傳送需要引入spring-boot-start-mail
  • SpringBoot 自動配置MailSenderAutoConfiguration
  • 定義MailProperties內容,配置在application.yml中
  • 自動裝配JavaMailSender
  • 測試郵件傳送

測試:

  1. 引入pom依賴

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

    看他引入的依賴,可以看到jakarta.mail

    <dependency>
       <groupId>com.sun.mail</groupId>
       <artifactId>jakarta.mail</artifactId>
       <version>1.6.7</version>
       <scope>compile</scope>
    </dependency>
    
  2. 檢視自動配置類:MailSenderAutoConfiguration

    這個類中存在bean,JavaMailSenderImpl

    然後我們去看下配置檔案

    @ConfigurationProperties(
        prefix = "spring.mail"
    )
    public class MailProperties {
        private static final Charset DEFAULT_CHARSET;
        private String host;
        private Integer port;
        private String username;
        private String password;
        private String protocol = "smtp";
        private Charset defaultEncoding;
        private Map<String, String> properties;
        private String jndiName;
    }
    
  3. 配置檔案

    spring.mail.username=406623380@qq.com
    spring.mail.password=你的qq授權碼
    spring.mail.host=smtp.qq.com
    # QQ需要配置ssl
    spring.mail.properties.mail.smtp.ssl.enable=true
    

    獲取授權碼:在QQ郵箱中的設定->賬戶->開啟pop3和smtp服務

  4. Spring單元測試

    @Autowired
    JavaMailSenderImpl mailSender;
    
    @Test
    public void contextLoads() {
        //郵件設定1:一個簡單的郵件
        SimpleMailMessage message = new SimpleMailMessage();
        message.setSubject("通知-明天來狂神這聽課");
        message.setText("今晚7:30開會");
    
        message.setTo("24736743@qq.com");
        message.setFrom("24736743@qq.com");
        mailSender.send(message);
    }
    
    @Test
    public void contextLoads2() throws MessagingException {
        //郵件設定2:一個複雜的郵件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    
        helper.setSubject("通知-明天來狂神這聽課");
        helper.setText("<b style='color:red'>今天 7:30來開會</b>",true);
    
        //傳送附件
        helper.addAttachment("1.jpg",new File(""));
        helper.addAttachment("2.jpg",new File(""));
    
        helper.setTo("24736743@qq.com");
        helper.setFrom("24736743@qq.com");
    
        mailSender.send(mimeMessage);
    }
    

    檢視郵箱,郵件接收成功!

    我們只需要使用Thymeleaf進行前後端結合即可開發自己網站郵件收發功能了!

14、Dubbo和Zookeeper整合

分散式理論

什麼是分散式系統?

在《分散式系統原理與範型》一書中有如下定義:“分散式系統是若干獨立計算機的集合,這些計算機對於使用者來說就像單個相關係統”;

分散式系統是由一組通過網路進行通訊、為了完成共同的任務而協調工作的計算機節點組成的系統。分散式系統的出現是為了用廉價的、普通的機器完成單個計算機無法完成的計算、儲存任務。其目的是利用更多的機器,處理更多的資料

分散式系統(distributed system)是建立在網路之上的軟體系統。

首先需要明確的是,只有當單個節點的處理能力無法滿足日益增長的計算、儲存任務的時候,且硬體的提升(加記憶體、加磁碟、使用更好的CPU)高昂到得不償失的時候,應用程式也不能進一步優化的時候,我們才需要考慮分散式系統。因為,分散式系統要解決的問題本身就是和單機系統一樣的,而由於分散式系統多節點、通過網路通訊的拓撲結構,會引入很多單機系統沒有的問題,為了解決這些問題又會引入更多的機制、協議,帶來更多的問題。。。

Dubbo文件

隨著網際網路的發展,網站應用的規模不斷擴大,常規的垂直應用架構已無法應對,分散式服務架構以及流動計算架構勢在必行,急需一個治理系統確保架構有條不紊的演進。

在Dubbo的官網文件有這樣一張圖

單一應用架構

當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用於簡化增刪改查工作量的資料訪問框架(ORM)是關鍵。

適用於小型網站,小型管理系統,將所有功能都部署到一個功能裡,簡單易用。

缺點:

1、效能擴充套件比較難

2、協同開發問題

3、不利於升級維護

垂直應用架構

當訪問量逐漸增大,單一應用增加機器帶來的加速度越來越小,將應用拆成互不相干的幾個應用,以提升效率。此時,用於加速前端頁面開發的Web框架(MVC)是關鍵。

通過切分業務來實現各個模組獨立部署,降低了維護和部署的難度,團隊各司其職更易管理,效能擴充套件也更方便,更有針對性。

缺點:公用模組無法重複利用,開發性的浪費

分散式服務架構

當垂直應用越來越多,應用之間互動不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提高業務複用及整合的分散式服務框架(RPC)是關鍵。

流動計算架構

當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個排程中心基於訪問壓力實時管理叢集容量,提高叢集利用率。此時,用於提高機器利用率的資源排程和治理中心(SOA)[ Service Oriented Architecture]是關鍵。

什麼是RPC

RPC【Remote Procedure Call】是指遠端過程呼叫,是一種程式間通訊方式,他是一種技術的思想,而不是規範。它允許程式呼叫另一個地址空間(通常是共享網路的另一臺機器上)的過程或函式,而不用程式設計師顯式編碼這個遠端呼叫的細節。即程式設計師無論是呼叫本地的還是遠端的函式,本質上編寫的呼叫程式碼基本相同。

也就是說兩臺伺服器A,B,一個應用部署在A伺服器上,想要呼叫B伺服器上應用提供的函式/方法,由於不在一個記憶體空間,不能直接呼叫,需要通過網路來表達呼叫的語義和傳達呼叫的資料。為什麼要用RPC呢?就是無法在一個程式內,甚至一個計算機內通過本地呼叫的方式完成的需求,比如不同的系統間的通訊,甚至不同的組織間的通訊,由於計算能力需要橫向擴充套件,需要在多臺機器組成的叢集上部署應用。RPC就是要像呼叫本地的函式一樣去調遠端函式;

RPC基本原理

步驟解析:

RPC兩個核心模組:通訊,序列化。

測試環境搭建

Dubbo

Apache Dubbo |ˈdʌbəʊ| 是一款高效能、輕量級的開源Java RPC框架,它提供了三大核心能力:面向介面的遠端方法呼叫,智慧容錯和負載均衡,以及服務自動註冊和發現。

dubbo官網 http://dubbo.apache.org/zh-cn/index.html

  1. 瞭解Dubbo的特性
  2. 檢視官方文件

dubbo基本概念

服務提供者(Provider):暴露服務的服務提供方,服務提供者在啟動時,向註冊中心註冊自己提供的服務。

服務消費者(Consumer):呼叫遠端服務的服務消費方,服務消費者在啟動時,向註冊中心訂閱自己所需的服務,服務消費者,從提供者地址列表中,基於軟負載均衡演算法,選一臺提供者進行呼叫,如果呼叫失敗,再選另一臺呼叫。

註冊中心(Registry):註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連線推送變更資料給消費者

監控中心(Monitor):服務消費者和提供者,在記憶體中累計呼叫次數和呼叫時間,定時每分鐘傳送一次統計資料到監控中心

呼叫關係說明

  1. 服務容器負責啟動,載入,執行服務提供者。

  2. 服務提供者在啟動時,向註冊中心註冊自己提供的服務。

  3. 服務消費者在啟動時,向註冊中心訂閱自己所需的服務。

  4. 註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連線推送變更資料給消費者。

  5. 服務消費者,從提供者地址列表中,基於軟負載均衡演算法,選一臺提供者進行呼叫,如果呼叫失敗,再選另一臺呼叫。

  6. 服務消費者和提供者,在記憶體中累計呼叫次數和呼叫時間,定時每分鐘傳送一次統計資料到監控中心。

Dubbo環境搭建

點進dubbo官方文件,推薦我們使用Zookeeper 註冊中心

Windows下安裝zookeeper

  1. 下載zookeeper :地址, 我們下載3.4.14 , 最新版!解壓zookeeper

  2. 執行/bin/zkServer.cmd ,初次執行會報錯,沒有zoo.cfg配置檔案;

    可能遇到問題:閃退 !

    解決方案:編輯zkServer.cmd檔案末尾新增pause 。這樣執行出錯就不會退出,會提示錯誤資訊,方便找到原因。

  3. 修改zoo.cfg配置檔案

    將conf資料夾下面的zoo_sample.cfg複製一份改名為zoo.cfg即可。

    注意幾個重要位置:

    dataDir=./ 臨時資料儲存的目錄(可寫相對路徑)

    clientPort=2181 zookeeper的埠號

    修改完成後再次啟動zookeeper

  4. 使用zkCli.cmd測試

    1. ls /:列出zookeeper根下儲存的所有節點
    [zk: 127.0.0.1:2181(CONNECTED) 4] ls /
    [zookeeper]
    
    1. create –e /kuangshen 123:建立一個kuangshen節點,值為123

    2. get /kuangshen:獲取/kuangshen節點的值

    我們再來檢視一下節點

windows下安裝dubbo-admin

dubbo本身並不是一個服務軟體。它其實就是一個jar包,能夠幫你的java程式連線到zookeeper,並利用zookeeper消費、提供服務。

但是為了讓使用者更好的管理監控眾多的dubbo服務,官方提供了一個視覺化的監控程式dubbo-admin,不過這個監控即使不裝也不影響使用。

我們這裡來安裝一下:

1、下載dubbo-admin

地址 :https://github.com/apache/dubbo-admin/tree/master

2、解壓進入目錄

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest

dubbo.registry.address=zookeeper://127.0.0.1:2181

3、在專案目錄下打包dubbo-admin

mvn clean package -Dmaven.test.skip=true

第一次打包的過程有點慢,需要耐心等待!直到成功!

4、執行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

【注意:zookeeper的服務一定要開啟!】

執行完畢,我們去訪問一下 http://localhost:7001/ , 這時候我們需要輸入登入賬戶和密碼,我們都是預設的root-root;

登入成功後,檢視介面,安裝完成!

SpringBoot + Dubbo + zookeeper

框架搭建

1. 啟動zookeeper !

2. IDEA建立一個空專案;

3.建立一個模組,實現服務提供者:provider-server , 選擇web依賴即可

4.專案建立完畢,我們寫一個服務,比如賣票的服務;

編寫介面

public interface TicketService {
    public String getTicket();
}

編寫實現類

public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神說Java》";
    }
}

5.建立一個模組,實現服務消費者:consumer-server , 選擇web依賴即可

6.專案建立完畢,我們寫一個服務,比如使用者的服務;

編寫service

package com.kuang.consumer.service;

public class UserService {
    //我們需要去拿去註冊中心的服務
}

需求:現在我們的使用者想使用買票的服務,這要怎麼弄呢 ?

服務提供者

1、將服務提供者註冊到註冊中心,我們需要整合Dubbo和zookeeper,所以需要導包

我們從dubbo官網進入github,看下方的幫助文件,找到dubbo-springboot,找到依賴包

<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.0.7</version>
</dependency>

zookeeper的包我們去maven倉庫下載,zkclient;

<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
   <groupId>com.github.sgroschupf</groupId>
   <artifactId>zkclient</artifactId>
   <version>0.1</version>
</dependency>

【新版的坑】zookeeper及其依賴包,解決日誌衝突,還需要剔除日誌依賴;

<!-- 引入zookeeper -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--排除這個slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2、在springboot配置檔案中配置dubbo相關屬性!

#當前應用名字
dubbo.application.name=provider-server
#註冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#掃描指定包下服務
dubbo.scan.base-packages=com.kuang.provider.service

3、在service的實現類中配置服務註解,釋出服務!注意導包問題

@DubboService //將服務釋出出去
@Component //放在容器中
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神說Java》";
    }
}

邏輯理解 :應用啟動起來,dubbo就會掃描指定的包下帶有@component註解的服務,將它釋出在指定的註冊中心中!

服務消費者

1、匯入依賴,和之前的依賴一樣;

<!--dubbo-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.0.7</version>
</dependency>
<!--zookeeper-->
<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>
<!-- 引入zookeeper -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--排除這個slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2、配置引數

#當前應用名字
dubbo.application.name=consumer-server
#註冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3、本來正常步驟是需要將服務提供者的介面打包,然後用pom檔案匯入,我們這裡使用簡單的方式,直接將服務的介面拿過來,路徑必須保證正確,即和服務提供者相同;

4、完善消費者的服務類

@DubboService //注入到容器中
public class UserService {

    @Reference //遠端引用指定的服務,他會按照全類名進行匹配,看誰給註冊中心註冊了這個全類名
    TicketService ticketService;

    public void bugTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在註冊中心買到"+ticket);
    }

}

5、測試類編寫;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    public void contextLoads() {

        userService.bugTicket();
    }

}

啟動測試

1. 開啟zookeeper

2. 開啟dubbo-admin實現監控【可以不用做】

3. 開啟服務者

4. 消費者消費測試,結果輸出“在註冊中心買到《狂神說Java》”,監控中心可以監控到服務者service

ok , 這就是SpingBoot + dubbo + zookeeper實現分散式開發的應用,其實就是一個服務拆分的思想;

相關文章