自定義Spring Shell

目錄

  • 概述
  • 自定義內置命令
    • 禁用內置命令
    • 覆蓋內置命令
  • 自定義命令提示符
  • 自定義命令行選項行為
  • 自定義參數轉換器

概述

官網:https://projects.spring.io/spring-shell/。

Spring Shell除了提供一些常用的內置命令之外,還允許開發者對一些默認功能進行定製。

自定義內置命令

禁用內置命令

禁用Spring Shell的內置命令非常簡單,只需要在pom.xml文件中進行簡單配置即可,如下所示:

<!-- Spring Shell -->
<dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell-starter</artifactId>
    <version>2.0.0.RELEASE</version>
    <exclusions>
        <!-- 禁用內置命令 -->
        <exclusion>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-standard-commands</artifactId>
        </exclusion>
    </exclusions>
</dependency>
shell:>help
No command found for 'help'
shell:>exit
No command found for 'exit'
shell:>

完全禁用了所有內置命令之後,將無法通過help命令查詢其他命令信息,也不能再使用exit命令退出應用。
因此,如果有需要的情況下,應該只是禁用某些內置命令。

如果需要禁用指定內置命令,需要在代碼中設置對應的命令屬性為false,格式為:spring.shell.command.<command>.enabled=true
例如,需要禁用help命令:

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        String[] disabledCommands = new String[]{"--spring.shell.command.help.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
# help命令將不再能使用
shell:>help
No command found for 'help'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>exit

如果禁用的是其他命令,如:clear,在Spring Shell應用啟動之後通過help命令不再能看被禁用的命令了。

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        // 禁用了內置的clear命令
        String[] disabledCommands = new String[]{"--spring.shell.command.clear.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

顯然,在禁用了指定的內置命令之後,通過help命令將不能看到該命令了。

覆蓋內置命令

如果希望重寫內置命令的實現,可以通過實現接口org.springframework.shell.standard.commands.<Command>.Command來完成(如:需要重寫clear命令的實現,實現接口org.springframework.shell.standard.commands.Clear.Command)。
如下為重寫內置命令script的實現:

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.commands.Script;
// 實現接口org.springframework.shell.standard.commands.Script.Command
@ShellComponent
public class MyScript implements Script.Command {
    // 注意:命令名稱與內置命令保持一致
    @ShellMethod("Read and execute commands from a file.")
    public void script() {
      / // 實現自定義邏輯
        System.out.println("override default script command");
    }
}

有意思的是,此時在內置命令“Built-In Commands”分組中將不能看到script命令了,而是在自定義的分組中,

shell:>help
AVAILABLE COMMANDS

Built-In Commands  # 在內置命令分組中看不到重寫的命令了
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        stacktrace: Display the full stacktrace of the last error.

My Script          # 重寫的命令此時在自定義分組中
        scriptdo: Read and execute commands from a file.

如果希望被覆蓋的內置命令依然能夠在“Built-In Commands”分組中看到,可以通過註解@ShellMethod的group屬性指定。

// 指定被覆蓋的內置命令分組為“Built-In Commands”
@ShellMethod(value = "Read and execute commands from a file.", group = "Built-In Commands")
public void script() {
    System.out.println("override default script command");
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

shell:>script
override default script command

自定義命令提示符

默認情況下,Spring Shell啟動之後显示的是一個黃色的命令提示符(shell:>)等待用戶輸入。
可以通過Spring Shell提供的接口org.springframework.shell.jline.PromptProvider對該命令提示符進行定製。

// 通過實現接口org.springframework.shell.jline.PromptProvider定製命令提示符
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.stereotype.Component;
@Component
public class MyPromptProvider implements PromptProvider {
    @Override
    public AttributedString getPrompt() {
        // 定製命令提示符為紅色的“#”
        return new AttributedString("#", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
    }
}

如下為定製的命令提示符:

自定義命令行選項行為

Spring Shell提供了2個默認的ApplicationRunner,用於實現命令行選項的行為。

1.InteractiveShellApplicationRunner用於啟動交互式界面,接收用戶輸入命令。
2.ScriptShellApplicationRunner用於在應用啟動時從程序參數中讀取指定文件中的命令並執行,具體來講:將多個命令寫在文件中,並通過參數的形式將包含了批量命令的文件路徑傳遞給程序,傳遞的文件路徑參數必須以“@”開始,如下示例:

$ java -jar /home/test/sun/workspace/test-springshell/target/test-springshell-0.0.1-SNAPSHOT.jar @/home/test/cmd

文件/home/test/cmd中的內容為:

$ cat /home/test/cmd 
help

這樣,在啟動程序時,將會自動執行/home/test/cmd文件中的命令(如果文件不存在,啟動應用時報錯)。
值得注意的是: 當在程序參數中存在“@local_file_path”這樣的參數時,應用啟動后執行完文件“local_file_path”內命令之後就退出了,不會進入交互式命令行界面(上述示例中,應用啟動后執行help命令之後就退出了)。

如果Spring Shell默認提供的上述2個ApplicationRunner無法滿足需求,可以自定義其他的命令行選項行為,直接實現接口org.springframework.boot.ApplicationRunner即可。

自定義參數轉換器

默認情況下,Spring Shell使用標準的Spring類型轉換機制將命令行的文本參數轉換為指定的類型。
實際上,Spring Shell是通過DefaultConversionService註冊Converter<S, T>GenericConverter或者ConverterFactory<S, R>類型的Bean對象來實現對命令行參數進行類型轉換的。

換句話說,如果我們需要自定義類型轉換器,只需要簡單實現接口org.springframework.core.convert.converter.Converter<S, T>就可以了。

// 自定義類型
public class Food {
    private String value = null;
    public Food(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append("Food{").append("value='").append(value).append("'}")
                .toString();
    }
}

// 自定義類型轉換器
@Component
public class MyConverter implements Converter<String, Food> {
    @Override
    public Food convert(String s) {
        // 將輸入參數轉換為Food類型實例
        return new Food(s);
    }
}

// 使用自定義轉換類型
@ShellComponent
public class ConvertionCmd {
    // 在命令方法中直接可以獲取Food對象,這是通過前面的自定義類型轉換器MyConverter實現的
    @ShellMethod("Conversion food")
    public String food(Food food) {
        return food.toString();
    }
}

在命令行指定命令food

#food apple
Food{value='apple'}

顯然,通過自定義類型轉換器可以實現對命令參數的特殊處理,非常實用。

【參考】
https://blog.csdn.net/zknxx/article/details/52196427 SpringBoot之CommandLineRunner接口和ApplicationRunner接口
https://www.jianshu.com/p/5d4ffe267596 CommandLineRunner或者ApplicationRunner接口

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

基於vue + axios + lrz.js 微信端圖片壓縮上傳

業務場景

微信端項目是基於Vux + Axios構建的,關於圖片上傳的業務場景有以下幾點需求:

1、單張圖片上傳(如個人頭像,實名認證等業務)
2、多張圖片上傳(如某類工單記錄)
3、上傳圖片時期望能按指定尺寸壓縮處理
4、上傳圖片可以從相冊中選擇或者直接拍照

遇到的坑

採用微信JSSDK上傳圖片

在之前開發的項目中(mui + jquery),有使用過微信JSSDK的接口上傳圖片,本想應該能快速遷移至此項目。事實證明編程沒有簡單的事:
1、按指定尺寸壓縮圖片
JSSDK提供的接口wx.chooseImage 是不能指定圖片壓縮尺寸的,只能在後端的接口通過localId獲取圖片時,再轉換成指定的尺寸。
2、微信JSSDK的接口權限驗證
只要是單頁面應用項目,微信JSSDK注入權限驗證都會有這個坑,而這個與路由模式(hash 或 history)也有關聯。有關此坑, 後續會再次寫文總結。參考解決方案[微信JSSDK] 解決SDK注入權限驗證 安卓正常,IOS出現config fail
經過權衡考慮網頁可能需要在微信以外的瀏覽器上也能上傳文件,顧後來放棄了採用微信JSSDK接口上傳圖片的方式。

android版微信,input onchange事件不觸發

這個坑,圈內有很多人踩過了。在PC端測試是正常的,發布之後,微信端上傳時能選擇文件,但之後沒有任何效果。日誌跟蹤,後台的api都未調用,由此判斷是input的onchange事件未被觸發。
解決方案, 更改input的 accept屬性:

<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />

將以上代碼更改為:

<input ref="file" type="file" accept="image/*" @change="selectImgs" />

如果不允許從相冊中選擇,只能拍照,增加capture=”camera”:

<input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs" />

(注:如果場景支持從相冊選擇或拍照,測試發現某些機型拍照后返回到了主頁。哈哈,也有可能是其他因素引起的問題,未做深究了)

使用Lrz.js壓縮圖片

目前手機拍照的圖片文件大小一般在3-4M,如果在上傳時不做壓縮處理會相當浪費流量並且佔用服務器的存儲空間(期望上傳原圖的另做討論)。如果能夠在前端壓縮處理,那肯定是最理想的方案。而lrz.js則提供了前端圖片文件的壓縮方案,並且可以指定尺寸壓縮。實測:3M左右的圖片文件,按寬度450px尺寸壓縮上傳后的文件大小在500kb左右,上傳時間2s以內。
其核心源碼,如下:

selectImgs () {
  let file = this.$refs.file.files[0]
  lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
    var xhr = new XMLHttpRequest()
    xhr.open('POST', 'http://xxx.com/upload')

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 304) {
        // 無論後端拋出何種錯誤,都會走這裏
        try {
          // 如果後端跑異常,則能解析成功, 否則解析不成功
          let resp = JSON.parse(xhr.responseText)
          console.log('response: ', resp)
        } catch (e) {
          this.imageUrl = xhr.responseText
        }
      }
    }

    // 添加參數
    rst.formData.append('folder', 'wxAvatar') // 保存的文件夾
    rst.formData.append('base64', rst.base64)
    // 觸发上傳
    xhr.send(rst.formData)

    return rst
  })
}

單個圖片上傳組件完整代碼,如下(注: icon圖標使用的是svg-icon組件):

<template>
  <div class="imgUploader">
    <section v-if="imageUrl"
             class="file-item ">
      <img :src="imageUrl"
           alt="">
      <span class="file-remove"
            @click="remove()">+</span>
    </section>
    <section v-else
             class="file-item">
      <div class="add">
        <svg-icon v-if="!text"
                  class="icon"
                  icon-class="plus" />
        <span v-if="text"
              class="text">{{text}}</span>
        <input type="file"
               accept="image/*"
               @change="selectImgs"
               ref="file">
      </div>
    </section>
  </div>
</template>

<script>
import lrz from 'lrz'
export default {
  props: {
    text: String,
    // 壓縮尺寸,默認寬度為450px
    size: {
      type: Number,
      default: 450
    }
  },
  data () {
    return {
      img: {
        name: '',
        src: ''
      },
      uploadUrl:  'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
      imageUrl: ''
    }
  },
  watch: {
    imageUrl (val, oldVal) {
      this.$emit('input', val)
    },
    value (val) {
      this.imageUrl = val
    }
  },
  mounted () {
    this.imageUrl = this.value
  },
  methods: {
    // 選擇圖片
    selectImgs () {
      let file = this.$refs.file.files[0]
      lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
        var xhr = new XMLHttpRequest()
        xhr.open('POST', this.uploadUrl)

        xhr.onload = () => {
          if (xhr.status === 200 || xhr.status === 304) {
            // 無論後端拋出何種錯誤,都會走這裏
            try {
              // 如果後端跑異常,則能解析成功, 否則解析不成功
              let resp = JSON.parse(xhr.responseText)
              console.log('response: ', resp)
            } catch (e) {
              this.imageUrl = xhr.responseText
            }
          }
        }

        // 添加參數
        rst.formData.append('folder', this.folder) // 保存的文件夾
        rst.formData.append('base64', rst.base64)
        // 觸发上傳
        xhr.send(rst.formData)

        return rst
      })
    },
    // 移除圖片
    remove () {
      this.imageUrl = ''
    }
  }
}
</script>

<style lang="less" scoped>
.imgUploader {
  margin-top: 0.5rem;
  .file-item {
    float: left;
    position: relative;
    width: 100px;
    text-align: center;
    left: 2rem;
    img {
      width: 100px;
      height: 100px;
      border: 1px solid #ececec;
    }
    .file-remove {
      position: absolute;
      right: 0px;
      top: 4px;
      width: 14px;
      height: 14px;
      color: white;
      cursor: pointer;
      line-height: 12px;
      border-radius: 100%;
      transform: rotate(45deg);
      background: rgba(0, 0, 0, 0.5);
    }

    &:hover .file-remove {
      display: inline;
    }
    .file-name {
      margin: 0;
      height: 40px;
      word-break: break-all;
      font-size: 14px;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
  }
  .add {
    width: 100px;
    height: 100px;
    float: left;
    text-align: center;
    line-height: 100px;
    font-size: 30px;
    cursor: pointer;
    border: 1px dashed #40c2da;
    color: #40c2da;
    position: relative;
    background: #ffffff;
    .icon {
      font-size: 1.4rem;
      color: #7dd2d9;
      vertical-align: -0.25rem;
    }
    .text {
      font-size: 1.2rem;
      color: #7dd2d9;
      vertical-align: 0.25rem;
    }
  }
}
input[type="file"] {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border: 1px solid #000;
  opacity: 0;
}
</style>

後端圖片存儲處理

後端api對圖片的處理,是必不可少的環節,需要將前端提交過來的base64字符串轉換成圖片格式,並存放至指定的文件夾,接口返回圖片的Url路徑。各項目後端對圖片的處理邏輯都不一致,以下方案僅供參考(我們使用asp.net MVC 構建了獨立的文件存儲站點)。
其核心源碼,如下:

/// <summary>
/// 圖片文件base64上傳
/// </summary>
/// <param name="folder">對應文件夾位置</param>
/// <param name="base64">圖片文件base64字符串</param>
/// <returns></returns>
public ActionResult Base64FileUpload(string folder, string base64)
{
    var context = System.Web.HttpContext.Current;
    context.Response.ClearContent();
    // 因為前端調用時,需要做跨域處理
    context.Response.AddHeader("Access-Control-Allow-Origin", "*");
    context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
    context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
    context.Response.AddHeader("Access-Control-Max-Age", "30");
    if (context.Request.HttpMethod.Equals("OPTIONS"))
    {
        return Content("");
    }

    var resultStr = base64.Substring(base64.IndexOf(",") + 1);//需要去掉頭部信息,這很重要
    byte[] bytes = Convert.FromBase64String(resultStr);
    var fileName = Guid.NewGuid().ToString() + ".png";
    if (folder.IsEmpty()) folder = "folder";
    //本地上傳
    string root = string.Format("/Resource/{0}/", folder);
    string virtualPath = root + fileName;
    string path = Server.MapPath("~" + virtualPath);
    //創建文件夾
    if (!Directory.Exists(Path.GetDirectoryName(path)))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
    }
    System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);//轉換成無法調整大小的MemoryStream對象
    System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
    bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);//保存到服務器路徑
    ms.Close();//關閉當前流,並釋放所有與之關聯的資源
    return Content(Net.Url + virtualPath); //返迴文件路徑
}

結語

由於項目實際情況,上述的方案中還存在諸多未完善的點:
1、多張圖片上傳,還是採用的與單張圖片相同的接口處理, 更為完善的方案是,前端的多圖上傳組件只綁定一個關聯Id,即可通過實現上傳和將圖片列表查詢展示(注:該功能在微信端未實現)。
2、後端圖片上傳的接口,未做嚴格的安全校驗,更為完善的方案是,每個上傳的場景,都應該限制文件類型,限制文件大小,以及文件數據來源校驗(注: 如軟件需要按二級等保標準測評,則後端接口會檢測通不過)。
3、上傳組件,未显示上傳進度,體驗性稍差。
正如前文所述,出於項目實際情況考慮,只是簡單實現圖片壓縮上傳功能,如要支持更多的場景,還得細細雕琢。

參考

1、移動端H5實現圖片上傳
2、安卓版微信 input onchange事件不生效

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

spring源碼解析之IOC容器(二)——加載和註冊

  上一篇跟蹤了IOC容器對配置文件的定位,現在我們繼續跟蹤代碼,看看IOC容器是怎麼加載和註冊配置文件中的信息的。開始之前,首先我們先來了解一下IOC容器所使用的數據結構——-BeanDefinition,它是一個上層接口,有很多實現類,分別對應不同的數據載體。我們平時開發的時候,也會定義很多pojo類,來作為獲取數據的載體。最常見的就是,從數據庫中獲取數據之後,使用一個定義的pojo來裝載,然後我們就可以在程序中使用這個pojo類來編寫各種業務邏輯。同樣,IOC容器首先會讀取配置的XML中各個節點,即各個標籤元素,然後根據不同的標籤元素,使用不同的數據結構來裝載該元素中的各種屬性的值。比如我們最熟悉的<bean>標籤,就是使用AbstractBeanDefinition這個數據結構,接下來的分析中我們可以看到。

  先回到上篇資源的定位那裡,代碼如下:

 1 public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
 2         ResourceLoader resourceLoader = getResourceLoader();
 3         if (resourceLoader == null) {
 4             throw new BeanDefinitionStoreException(
 5                     "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
 6         }
 7 
 8         if (resourceLoader instanceof ResourcePatternResolver) {
 9             // Resource pattern matching available.
10             try {
11                 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
12                 int loadCount = loadBeanDefinitions(resources);
13                 if (actualResources != null) {
14                     for (Resource resource : resources) {
15                         actualResources.add(resource);
16                     }
17                 }
18                 if (logger.isDebugEnabled()) {
19                     logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
20                 }
21                 return loadCount;
22             }
23             catch (IOException ex) {
24                 throw new BeanDefinitionStoreException(
25                         "Could not resolve bean definition resource pattern [" + location + "]", ex);
26             }
27         }
28         else {
29             // 定位到資源之後,封裝成一個resource對象
30             Resource resource = resourceLoader.getResource(location);
31             int loadCount = loadBeanDefinitions(resource);
32             if (actualResources != null) {
33                 actualResources.add(resource);
34             }
35             if (logger.isDebugEnabled()) {
36                 logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
37             }
38             return loadCount;
39         }
40     }

  進入loadBeanDefinitions(resource)方法,正式開始加載源碼的跟蹤:

1         @Override
2     public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
3         return loadBeanDefinitions(new EncodedResource(resource));
4     }    
 1 public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
 2         Assert.notNull(encodedResource, "EncodedResource must not be null");
 3         if (logger.isInfoEnabled()) {
 4             logger.info("Loading XML bean definitions from " + encodedResource);
 5         }
 6 
 7         Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
 8         if (currentResources == null) {
 9             currentResources = new HashSet<EncodedResource>(4);
10             this.resourcesCurrentlyBeingLoaded.set(currentResources);
11         }
12         if (!currentResources.add(encodedResource)) {
13             throw new BeanDefinitionStoreException(
14                     "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
15         }
16         try {
17             InputStream inputStream = encodedResource.getResource().getInputStream();
18             try {
19                 InputSource inputSource = new InputSource(inputStream);
20                 if (encodedResource.getEncoding() != null) {
21                     inputSource.setEncoding(encodedResource.getEncoding());
22                 }
23                 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
24             }
25             finally {
26                 inputStream.close();
27             }
28         }
29         catch (IOException ex) {
30             throw new BeanDefinitionStoreException(
31                     "IOException parsing XML document from " + encodedResource.getResource(), ex);
32         }
33         finally {
34             currentResources.remove(encodedResource);
35             if (currentResources.isEmpty()) {
36                 this.resourcesCurrentlyBeingLoaded.remove();
37             }
38         }
39     }

  進入doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法:

 1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
 2             throws BeanDefinitionStoreException {
 3         try {
 4             Document doc = doLoadDocument(inputSource, resource);
 5             return registerBeanDefinitions(doc, resource);
 6         }
 7         catch (BeanDefinitionStoreException ex) {
 8             throw ex;
 9         }
10         catch (SAXParseException ex) {
11             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
12                     "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
13         }
14         catch (SAXException ex) {
15             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
16                     "XML document from " + resource + " is invalid", ex);
17         }
18         catch (ParserConfigurationException ex) {
19             throw new BeanDefinitionStoreException(resource.getDescription(),
20                     "Parser configuration exception parsing XML from " + resource, ex);
21         }
22         catch (IOException ex) {
23             throw new BeanDefinitionStoreException(resource.getDescription(),
24                     "IOException parsing XML document from " + resource, ex);
25         }
26         catch (Throwable ex) {
27             throw new BeanDefinitionStoreException(resource.getDescription(),
28                     "Unexpected exception parsing XML document from " + resource, ex);
29         }
30     }

  繼續進入registerBeanDefinitions(doc, resource)方法:

1 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
2         //此時documentReader已經是DefaultBeanDefinitionDocumentReader類了
3         BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
4         int countBefore = getRegistry().getBeanDefinitionCount();
5         documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
6         //返回當前註冊的beanDefinition的個數
7         return getRegistry().getBeanDefinitionCount() - countBefore;
8     }

  進入registerBeanDefinitions(doc, createReaderContext(resource))方法:

1 public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
2         this.readerContext = readerContext;
3         logger.debug("Loading bean definitions");
4         Element root = doc.getDocumentElement();
5         doRegisterBeanDefinitions(root);
6     }

  進入doRegisterBeanDefinitions(root)方法:

 1 protected void doRegisterBeanDefinitions(Element root) {
 2         // Any nested <beans> elements will cause recursion in this method. In
 3         // order to propagate and preserve <beans> default-* attributes correctly,
 4         // keep track of the current (parent) delegate, which may be null. Create
 5         // the new (child) delegate with a reference to the parent for fallback purposes,
 6         // then ultimately reset this.delegate back to its original (parent) reference.
 7         // this behavior emulates a stack of delegates without actually necessitating one.
 8         BeanDefinitionParserDelegate parent = this.delegate;
 9         this.delegate = createDelegate(getReaderContext(), root, parent);
10 
11         if (this.delegate.isDefaultNamespace(root)) {
12             //profile屬性平時使用非常少,該屬性可以用於配置數據庫的切換(常用),使用時,需要在web.xml中配置context-parm
13             //<context-parm>
14             //    <parm-name>Spring.profiles.active</parm-name>
15             //    <parm-value>dev(在applicationContext.xml中配置的profile屬性的beans的profile屬性值)</parm-name>
16             //</context-parm>
17             //在applicationContext.xml中的配置
18             //<beans profile="dev">    </beans>
19             //<beans profile="produce">   </beans>
20             String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
21             if (StringUtils.hasText(profileSpec)) {
22                 String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
23                         profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
24                 if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
25                     if (logger.isInfoEnabled()) {
26                         logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
27                                 "] not matching: " + getReaderContext().getResource());
28                     }
29                     return;
30                 }
31             }
32         }
33 
34         preProcessXml(root);
35         parseBeanDefinitions(root, this.delegate);
36         postProcessXml(root);
37 
38         this.delegate = parent;
39     }

  這裏也用到了模板方法,preProcessXml(root)和postProcessXml(root)這兩個方法都是空實現,是留給客戶來實現自己的邏輯的。重點研究一下parseBeanDefinitions(root, this.delegate)方法:

 1 protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
 2         if (delegate.isDefaultNamespace(root)) {
 3             NodeList nl = root.getChildNodes();
 4             for (int i = 0; i < nl.getLength(); i++) {
 5                 Node node = nl.item(i);
 6                 if (node instanceof Element) {
 7                     Element ele = (Element) node;
 8                     if (delegate.isDefaultNamespace(ele)) {
 9                         parseDefaultElement(ele, delegate);
10                     }
11                     else {
12                         delegate.parseCustomElement(ele);
13                     }
14                 }
15             }
16         }
17         else {
18             delegate.parseCustomElement(root);
19         }
20     }

  parseCustomElement(root)方法不需要怎麼研究,我們平時幾乎不會用到自定義的標籤,所以只跟蹤parseDefaultElement(ele, delegate)裏面的代碼:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    //import標籤
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
            importBeanDefinitionResource(ele);
        }
    //alias標籤
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
            processAliasRegistration(ele);
        }
   //bean標籤
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
            processBeanDefinition(ele, delegate);
        }
  //beans標籤
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

  可以看到,對於不同的標籤,spring採用不同的策略進行處理,重點跟蹤一下處理bean標籤的方法processBeanDefinition(ele, delegate):

 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 2         //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
 3         //並將解析好的內容封裝成BeanDefinitionHolder對象
 4         BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 5         if (bdHolder != null) {
 6             bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
 7             try {
 8                 // Register the final decorated instance.
 9                 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10             }
11             catch (BeanDefinitionStoreException ex) {
12                 getReaderContext().error("Failed to register bean definition with name '" +
13                         bdHolder.getBeanName() + "'", ele, ex);
14             }
15             // Send registration event.
16             getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17         }
18     }

  在這個方法中,delegate.parseBeanDefinitionElement(ele)是解析bean元素中各種屬性的方法,registerBeanDefinition(bdHolder, getReaderContext().getRegistry())是將封裝好的數據進行存儲的方法。先看一下解析的方法:

 1 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
 2         //獲取bean標籤的id屬性的值
 3         String id = ele.getAttribute(ID_ATTRIBUTE);
 4         //獲取bean標籤上name屬性的值
 5         String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
 6 
 7         List<String> aliases = new ArrayList<String>();
 8         if (StringUtils.hasLength(nameAttr)) {
 9             //將name的值進行分割,並將它們當作別名存到aliases中
10             String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
11             aliases.addAll(Arrays.asList(nameArr));
12         }
13 
14         String beanName = id;
15         //如果bean標籤的id沒有值,但是name屬性有值,則將name屬性的第一個值當作id的值,並從aliases中將第一個別名移除掉
16         if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
17             beanName = aliases.remove(0);
18             if (logger.isDebugEnabled()) {
19                 logger.debug("No XML 'id' specified - using '" + beanName +
20                         "' as bean name and " + aliases + " as aliases");
21             }
22         }
23 
24         if (containingBean == null) {
25             //檢查bean的唯一性
26             checkNameUniqueness(beanName, aliases, ele);
27         }
28 
29         //這裏已經是將XML中bean元素中的所有屬性都封裝到beanDefinition對象中了
30         AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
31         if (beanDefinition != null) {
32             if (!StringUtils.hasText(beanName)) {
33                 try {
34                     if (containingBean != null) {
35                         beanName = BeanDefinitionReaderUtils.generateBeanName(
36                                 beanDefinition, this.readerContext.getRegistry(), true);
37                     }
38                     else {
39                         beanName = this.readerContext.generateBeanName(beanDefinition);
40                         // Register an alias for the plain bean class name, if still possible,
41                         // if the generator returned the class name plus a suffix.
42                         // This is expected for Spring 1.2/2.0 backwards compatibility.
43                         String beanClassName = beanDefinition.getBeanClassName();
44                         if (beanClassName != null &&
45                                 beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
46                                 !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
47                             aliases.add(beanClassName);
48                         }
49                     }
50                     if (logger.isDebugEnabled()) {
51                         logger.debug("Neither XML 'id' nor 'name' specified - " +
52                                 "using generated bean name [" + beanName + "]");
53                     }
54                 }
55                 catch (Exception ex) {
56                     error(ex.getMessage(), ele);
57                     return null;
58                 }
59             }
60             String[] aliasesArray = StringUtils.toStringArray(aliases);
61             //最後將封裝好的beanDefinition、它的id、以及它的別名一起封裝成BeanDefinitionHolder對象返回
62             return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
63         }
64 
65         return null;
66     }

  我們可以得到如下信息:

  1、獲取bean標籤的id屬性和name屬性的值;

  2、name屬性是可以都有多個值的,以逗號或者分號分割;

  3、如果id沒有賦值,則取name的第一個值作為id的值。所以,我們一般都會給id賦值,這樣效率高一些;

  4、檢查以這個id標識的bean是不是唯一的;

  5、進行其他屬性的解析,並最終封裝測AbstractBeanDefinition對象,也就是我們前文中提到的數據結構;

  6、最後封裝成BeanDefinitionHolder對象之後返回。

  進入parseBeanDefinitionElement(ele, beanName, containingBean)方法,看一下其他元素的解析過程:

 1 public AbstractBeanDefinition parseBeanDefinitionElement(
 2             Element ele, String beanName, BeanDefinition containingBean) {
 3 
 4         this.parseState.push(new BeanEntry(beanName));
 5 
 6         String className = null;
 7         if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
 8             className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
 9         }
10 
11         try {
12             String parent = null;
13             if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
14                 parent = ele.getAttribute(PARENT_ATTRIBUTE);
15             }
16             AbstractBeanDefinition bd = createBeanDefinition(className, parent);
17 
18             parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
19             bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
20 
21             parseMetaElements(ele, bd);
22             parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
23             parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
24 
25             parseConstructorArgElements(ele, bd);
26             parsePropertyElements(ele, bd);
27             parseQualifierElements(ele, bd);
28 
29             bd.setResource(this.readerContext.getResource());
30             bd.setSource(extractSource(ele));
31 
32             return bd;
33         }
34         catch (ClassNotFoundException ex) {
35             error("Bean class [" + className + "] not found", ele, ex);
36         }
37         catch (NoClassDefFoundError err) {
38             error("Class that bean class [" + className + "] depends on not found", ele, err);
39         }
40         catch (Throwable ex) {
41             error("Unexpected failure during bean definition parsing", ele, ex);
42         }
43         finally {
44             this.parseState.pop();
45         }
46 
47         return null;
48     }

  解析封裝成BeanDefinitionHolder對象之後,就可以進行註冊了,先回到之前的processBeanDefinition(ele, delegate):

 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 2         //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
 3         //並將解析好的內容封裝成BeanDefinitionHolder對象
 4         BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 5         if (bdHolder != null) {
 6             bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
 7             try {
 8                 // Register the final decorated instance.
 9                 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10             }
11             catch (BeanDefinitionStoreException ex) {
12                 getReaderContext().error("Failed to register bean definition with name '" +
13                         bdHolder.getBeanName() + "'", ele, ex);
14             }
15             // Send registration event.
16             getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17         }
18     }

  現在進入BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())方法進行分析:

 1 public static void registerBeanDefinition(
 2             BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
 3             throws BeanDefinitionStoreException {
 4 
 5         // Register bean definition under primary name.
 6         String beanName = definitionHolder.getBeanName();
 7         registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
 8 
 9         // Register aliases for bean name, if any.
10         String[] aliases = definitionHolder.getAliases();
11         if (aliases != null) {
12             for (String alias : aliases) {
13                 registry.registerAlias(beanName, alias);
14             }
15         }
16     }

  這裏的beanName就是之前封裝好的bean的id。這個方法中分別以id和別名作為key來註冊bean,其實就是存儲在map中。

  進入registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()),在其子類DefaultListableBeanFactory中有實現:

 1 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
 2             throws BeanDefinitionStoreException {
 3 
 4         Assert.hasText(beanName, "Bean name must not be empty");
 5         Assert.notNull(beanDefinition, "BeanDefinition must not be null");
 6 
 7         if (beanDefinition instanceof AbstractBeanDefinition) {
 8             try {
 9                 ((AbstractBeanDefinition) beanDefinition).validate();
10             }
11             catch (BeanDefinitionValidationException ex) {
12                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
13                         "Validation of bean definition failed", ex);
14             }
15         }
16 
17         BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
18         if (existingDefinition != null) {
19             if (!isAllowBeanDefinitionOverriding()) {
20                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
21                         "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
22                         "': There is already [" + existingDefinition + "] bound.");
23             }
24             else if (existingDefinition.getRole() < beanDefinition.getRole()) {
25                 // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
26                 if (logger.isWarnEnabled()) {
27                     logger.warn("Overriding user-defined bean definition for bean '" + beanName +
28                             "' with a framework-generated bean definition: replacing [" +
29                             existingDefinition + "] with [" + beanDefinition + "]");
30                 }
31             }
32             else if (!beanDefinition.equals(existingDefinition)) {
33                 if (logger.isInfoEnabled()) {
34                     logger.info("Overriding bean definition for bean '" + beanName +
35                             "' with a different definition: replacing [" + existingDefinition +
36                             "] with [" + beanDefinition + "]");
37                 }
38             }
39             else {
40                 if (logger.isDebugEnabled()) {
41                     logger.debug("Overriding bean definition for bean '" + beanName +
42                             "' with an equivalent definition: replacing [" + existingDefinition +
43                             "] with [" + beanDefinition + "]");
44                 }
45             }
46             this.beanDefinitionMap.put(beanName, beanDefinition);
47         }
48         else {
49             if (hasBeanCreationStarted()) {
50                 // Cannot modify startup-time collection elements anymore (for stable iteration)
51                 synchronized (this.beanDefinitionMap) {
52                     this.beanDefinitionMap.put(beanName, beanDefinition);
53                     List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
54                     updatedDefinitions.addAll(this.beanDefinitionNames);
55                     updatedDefinitions.add(beanName);
56                     this.beanDefinitionNames = updatedDefinitions;
57                     if (this.manualSingletonNames.contains(beanName)) {
58                         Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
59                         updatedSingletons.remove(beanName);
60                         this.manualSingletonNames = updatedSingletons;
61                     }
62                 }
63             }
64             else {
65                 // Still in startup registration phase
66                 this.beanDefinitionMap.put(beanName, beanDefinition);
67                 this.beanDefinitionNames.add(beanName);
68                 this.manualSingletonNames.remove(beanName);
69             }
70             this.frozenBeanDefinitionNames = null;
71         }
72 
73         if (existingDefinition != null || containsSingleton(beanName)) {
74             resetBeanDefinition(beanName);
75         }
76     }

  我們可以看到:這個beanDefinitionMap就是用來存儲解析好的bean的,以id作為key。至此,就將所有的bean標籤解析好之後封裝成BeanDefinition註冊到了IOC容器中。但是,到目前為止,IOC容器並沒有為我們將這些解析好的數據生成一個一個bean實例,我們仍然不能就這樣直接使用。下一篇接着跟蹤。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

【拆分版】Docker-compose構建Elasticsearch 7.1.0集群

寫在前邊

搞了兩三天了,一直有個問題困擾着我,ES集群中配置怎麼能正確映射到主機上,這邊經常報ClusterFormationFailureHelper master not discovered or elected yet.原因是容器中的ES節點沒有正確的映射到主機上,而且容器內ip是易變的,我該怎麼配置呢?

臨下班了,終於想到個法子,固定容器ip——使用network_mode: host

看到主機模式的我眼前一亮,容器就相當於一個主機服務,你占哪個端口就是哪個,沒有必要再去自己指定port去映射到主機。這樣只要主機ip不變,容器中ip不就沒問題了么!!!

本文內容架構圖

解釋:

Master節點作為Master節點與協調節點,為防止腦裂問題,降低負載,不存數據

Node1~Node3為數據節點,不參與Master競選

TribeNode節點不存數據,不參與Master競選

準備環境

  • GNU/Debain Stretch 9.9 linux-4.19
  • Docker 18.09.6
  • Docker-Compose 1.17.1
  • elasticsearch:7.1.0

配置腳本參見我的Github <https://github.com/hellxz/docker-es-cluster.git>

宿主機環境準備參考ELK集群搭建,基於7.1.1 文中開始搭建的前四步

目錄結構

.
├── docker-es-data01
│   ├── data01
│   ├── data01-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-data02
│   ├── data02
│   ├── data02-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-data03
│   ├── data03
│   ├── data03-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-master
│   ├── docker-compose.yml
│   ├── .env
│   ├── es-config
│   │   └── elasticsearch.yml
│   ├── master-data
│   └── master-logs
└── docker-es-tribe
    ├── docker-compose.yml
    ├── .env
    ├── es-config
    │   └── elasticsearch.yml
    ├── tribe-data
    └── tribe-logs

最終效果

各目錄代表節點與端口號

節點目錄 節點名稱 協調端口號 說明 查詢端口號
docker-es-data01 data01 9301 數據節點1,非master節點 9201
docker-es-data02 data02 9302 數據節點2,非master節點 9202
docker-es-data03 data03 9303 數據節點3,非master節點 9203
docker-es-master master 9300 master節點,非數據節點 9200
docker-es-tribe tribe 9304 協調節點,非master非數據節點 9204

想測試這些節點是否可用,只需要修改每個節點目錄下的es-config/elasticsearch.yml中的ip地址,全部換成你需要的ip即可。

各文件功用舉例說明

鑒於這裏邊有很多是重複操作,這裏僅拿其中的master節點進行舉例,其餘代碼參見Github

.env 這個文件為docker-compose.yml提供默認參數,方便修改

# the default environment for es-master
# set es node jvm args
ES_JVM_OPTS=-Xms256m -Xmx256m
# set master node data folder
MASTER_DATA_DIR=./master-data
# set master node logs folder
MASTER_LOGS_DIR=./master-logs

docker-compose.yml docker-compose的配置文件

version: "3"
services:
    es-master:
        image: elasticsearch:7.1.0
        container_name: es-master
        environment: # setting container env
            - ES_JAVA_OPTS=${ES_JVM_OPTS}   # set es bootstrap jvm args
        restart: always
        volumes:
            - ./es-config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
            - ${MASTER_DATA_DIR}:/usr/share/elasticsearch/data:rw
            - ${MASTER_LOGS_DIR}:/usr/share/elasticsearch/logs:rw
        network_mode: "host"

簡單來說,就是修改pull的鏡像,替換其中的變量與配置文件,掛載數據與日誌目錄,最後用的host主機模式,讓節點服務佔用到實體機端口

elaticsearch.yml elasticsearch的配置文件,搭建集群最關鍵的文件之一

# ======================== Elasticsearch Configuration =========================
cluster.name: es-cluster
node.name: master 
node.master: true
node.data: false
node.attr.rack: r1 
bootstrap.memory_lock: true 
http.port: 9200
network.host: 10.2.114.110
transport.tcp.port: 9300
discovery.seed_hosts: ["10.2.114.110:9301","10.2.114.110:9302","10.2.114.110:9303","10.2.114.110:9304"] 
cluster.initial_master_nodes: ["master"] 
gateway.recover_after_nodes: 2

按照前幾篇文章下來,大家對這些參數已經不是很陌生了,這裏簡單說下幾個比較重要的參數

  • transport.tcp.port 設置es多節點協調的端口號
  • discovery.seed_hosts 設置當前節點啟動后要發現的協調節點位置,當然自己不需要發現自己,推薦使用ip:port形式,集群形成快
  • cluster.initial_master_nodes 集群中可以成為master節點的節點名,這裏指定唯一的一個,防止腦裂

使用說明

  1. 若想將此腳本使用到生產上,需要修改每個節點下的.env文件,將掛載數據、日誌目錄修改為啟動es的集群的用戶可讀寫的位置,可以通過sudo chmod 777 -R 目錄sudo chown -R 當前用戶名:用戶組 目錄 來修改被掛載的目錄權限
  2. 修改.env下的JVM參數,擴大堆內存,啟動與最大值最好相等,以減少gc次數,提高效率
  3. 修改所有節點下的docker-compose.yml 中的network.host地址 為當前所放置的主機的ip,discovery.seed_hosts需要填寫具體各待發現節點的實體機ip,以確保可以組成集群
  4. 確保各端口在其宿主機上沒有被佔用,如有佔用需確認是否有用,無用kill,有用則更新docker-compose.ymlhttp.porttransport.tcp.port,注意與此同時要更新其它節點的discovery.seed_hosts對應的port
  5. 如果在同一台主機上,可以參考使用文章後邊的簡單的shell腳本

各節點操作命令

後台啟動命令均為docker-compose up -d

關閉命令:

  • docker-compose down: 關閉同時移除容器與多餘虛擬網卡
  • docker stop contains_name: 根據容器名稱關閉容器,不移除容器

簡單的Shell腳本

docker-es-cluster-up.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for start up the docker-es-cluster designed in the one of linux server.
cd docker-es-master && docker-compose up -d && \
cd ../docker-es-data01 && docker-compose up -d && \
cd ../docker-es-data02 && docker-compose up -d && \
cd ../docker-es-data03 && docker-compose up -d && \
cd ../docker-es-tribe && docker-compose up -d && \
cd ..

docker-es-cluster-down.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for remove the docker-es-cluster's containers and networks designed in the one of linux server.
cd docker-es-tribe && docker-compose down && \
cd ../docker-es-data03 && docker-compose down && \
cd ../docker-es-data02 && docker-compose down && \
cd ../docker-es-data01 && docker-compose down && \
cd ../docker-es-master && docker-compose down && \
cd ..

docker-es-cluster-stop.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for stop the docker-es-cluster's containers designed in the one of linux server.
docker stop es-tribe es-data03 es-data02 es-data01 es-master

如果你想讓這些腳本有執行權限,不妨試試sudo chmod +x *.sh

這些腳本中沒有使用sudo,如需要使用sudo才能啟動docker,請添加當前用戶到docker組

Enjoy.

本文系原創文章,禁止轉載。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

來阿里有段時間了,談談這幾個月最真實的感受

作者:黃小斜

文章來源:微信公眾號【程序員江湖】

 

閱讀本文大概需要 4 分鐘。

 

 

有段時間沒寫過原創了,想了各種理由,發現其實理由就一個,沒時間。

 

我來阿里,已經幾個月了。這段時間,最大的感受就是累。我是在今年的四月份加入阿里的。

 

當初我沒有參加阿里巴巴的實習,而是選擇了直接進行校園招聘,這也是因為當時我對實習的部門不感興趣,於是在校招的時候我就選擇了自己感興趣的部門,也就是現在我所在的螞蟻金服。

 

之前就聽說過阿里的工作強度,可能會比較大,但是,當我在真正來到這家公司的時候,才發現996,並不是虛言,這倒不是說制度上規定的 996 ,而是因為,你手上的工作,是很難做完的,所以導致你不得不用加班的方式來完成,996 最終的目的只有一個,就是你要把手上的工作做完,對其他人有一個交代。

 

剛來一個月的時候只其實我是非常不適應的,畢竟從學校到公司,整個變化是顛覆性的,雖然之前有過實習經歷,但是是比較划水的,和正式工作差別還是比較大的,第一個月大抵的狀態就是,每天九點多上班,晚上九點及以後下班,這是生活規律上的不適應。

 

除此之外,在工作上也會有一些不適應,雖然前期給我的活兒都比較的零散,只要求這裏修修補補,那裡加點東西。這倒是其次。

 

 

主要的原因還是因為我對業務線的研發流程不太了解,以及對大家在做的事情不太了解,這會導致我在工作上遇到很多困難。因為我會聽不懂大家在說什麼。所以第一個月,更多的是思考。

 

如何去提高自己的效率,如何去融入這個公司,如何去熟悉部門的業務和技術戰,自己感覺是一個比較外向的人,所以一旦有問題就會諮詢,我的同事,我的師兄。

 

阿里的師兄是一種文化。就像武俠小說里的同門師兄弟一樣,師兄會帶領着你去做那些工作,並且讓你去熟悉整個部門的一些事情。我的師兄是一個寡言少語的人,但是技術能力非常強,也很有責任心。他總是對總是對工作要求特別高,不放過任何一個問題,不漏掉任何一行問題代碼。

 

在第一個月的時間里。我學習了很多阿里巴巴的一些中間件技術。其實這些技術。在學校複習的時候,多少也聽說過一些,看過一些文章。只不過外面用的都是開源的東西,而在阿里內部,更多的是自研的產品,比如說,消息中間件,分佈式事務,數據庫中間件,等等等等。這些東西,在平時的學習和研發中,是經常會用到的,如果你在學校或者是在其他小公司,可能根本沒有機會接觸到這些東西。

 

 

作為一個Java工程師,在這家公司的職責可不止是寫代碼,你需要熟悉整個研發流程,從系統設計,代碼開發,測試聯調,發布上線,問題排查都是你的職責,其實這很鍛煉人,這也是為什麼加班會這麼多的原因,寫代碼只佔你日常工作的一小部分時間,你需要花更多時間在解決各類問題上。

 

阿里對校招生有一個培訓,不同部門可能不太一樣,螞蟻的培訓長達一個月,這個月應該也是我覺得成長最快,過得最快樂的一個月,其實無非就是上課,拓展和各類活動,這段時間認識了很多人,其中也有很多大牛,大家一起上課,一起做項目,同甘共苦度過一個月,這樣的經歷也是非常值得紀念的。

 

培訓結束后,我也開始承擔更多的工作,當自己逐漸習慣這種節奏之後,才感覺自己逐漸在融入這家公司,每次搞懂一個業務問題或者技術問題都會覺得自己在成長,當肩頭上承擔更多責任的時候,同時也承擔了更多壓力,如果不能調整好自己的心態,我想在這家公司是很難待久的。

 

可能你在其他互聯網公司也會感受到相似的壓力,但這就是大部分互聯網公司的現狀,追求效率,追求極致,我們身在其中,就必須適應環境,尊重遊戲規則,馬上又是新一年的校招季,去年這個時候,這個公眾號才剛剛誕生,轉眼一年時間,多的是更多思考,希望公眾號的文章對你們有會有更多幫助。

 

下一篇文章應該也不會讓你們等太久。

 

 

文能碼字,武能coding,是我黃小斜,不是黃老邪噢。

 

 

推薦閱讀:

 

焦慮的 BAT、不安的編程語言,揭秘程序員技術圈生存現狀!

 

 

 

為什麼有些大公司技術弱爆了?

 

 

 

  

 

​你點的每個好看,我都認真當成了喜歡

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

kubernetes高級之動態准入控制

系列目錄

動態准入控制器文檔介紹了如何使用標準的,插件式的准入控制器.但是,但是由於以下原因,插件式的准入控制器在一些場景下並不靈活:

  • 它們需要編譯到kube-apiserver里

  • 它們僅在apiserver啟動的時候可以配置

准入鈎子(Admission Webhooks 從1.9版本開始)解決了這些問題,它允許准入控制器獨立於核心代碼編譯並且可以在運行時配置.

什麼是准入鈎子

准入鈎子是一種http回調,它接收准入請求然後做一些處理.你可以定義兩種類型的准入鈎子:驗證鈎子和變換鈎子.對於驗證鈎子,你可以拒絕請求以使自定義准入策略生效.對於變換鈎子,你可以改變請求來使自定義的默認配置生效.

體驗准入鈎子

准入控制鈎子是集群管制面板不可缺少的一部分.你在編寫部署它們時必須要警惕.如果你想要編寫/布置生產級別的准入控制器,請閱讀以下用戶指南.下面我們將介紹如何快速體驗准入鈎子.

準備工作:

  • 確保你的kubernetes集群版本至少是1.9版本.

  • 確保變換鈎子(MutatingAdmissionWebhook) 和驗證鈎子(ValidatingAdmissionWebhook)已經啟用.這裡是推薦開啟的一組准入控制器.

編寫一個准入鈎子服務器(admission webhook server)

請參閱已經被kubernetes e2e測試驗證通過的准入服務器鈎子( admission webhook server)的實現.這個web鈎子處理apiserver發出的admissionReview請求,然後把結果封裝成一個admissionResponse返回給請求者.

admissionReview請求可能有多個版本( v1beta1 或者 未來的v1),web鈎子可以通過admissionReviewVersions字段來定義它們接受的版本.apiserver會嘗試使用列表中出現的,支持的第一個版本.如果列表中的版本沒有一個是被支持的,驗證將失敗.如果webhook配置已經持久化,對web鈎子的請求將會失敗並被失敗策略控制.

示例鈎子服務器(admission webhook server)把ClientAuth字段留空,默認為NoClientCert.這意味着鈎子服務器不驗證客戶端身份.如果你需要使用mutual TLS或者其它方法來驗證客戶端請求,請參考如何認證apiserver

部署准入控制服務

e2e測試的鈎子服務器通過部署api(https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#deployment-v1beta1-apps)被部署到kubernetes集群中.測試項目也為鈎子服務器創建了一個前端服務,代碼

你也可以把你的鈎子服務部署到集群外,你需要相應地更新web鈎子客戶端配置

運行時配置准入web鈎子

你可以通過ValidatingWebhookConfiguration和MutatingWebhookConfiguration動態地配置哪些資源被哪些web鈎子控制.

以下是一個validatingWebhookConfiguration配置的示例,變換鈎子的配置也類似

apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: <name of this configuration object>
webhooks:
- name: <webhook name, e.g., pod-policy.example.io>
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: "Namespaced"
  clientConfig:
    service:
      namespace: <namespace of the front-end service>
      name: <name of the front-end service>
    caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
  admissionReviewVersions:
  - v1beta1
  timeoutSeconds: 1

scope字段指定了集群級別的資源(“Cluster”)或者名稱空間級別的資源(“Namespaced”)需要匹配這些規則.”*”表示沒有任何範圍限制.

注意,如果使用clientConfig.service,服務端證書必須對<svc_name>.<svc_namespace>.svc有效.

web鈎子請求默認超時時間為30秒,但是從1.14版本開始,你可以自由設置超時時間但是建議設置較小的時間.如果web鈎子請求超時,請求將被web鈎子的失敗策略處理.

當apiserver接收到一個匹配規則的請求,apiserver將會發送一個admissionReview請求到clientConfig配置的web鈎子里.

創建web鈎子配置以後,系統將會經過一段時間使新配置生效.

認證apiserver

如果你的准入web鈎子需要認證,你可以配置apiserver使用基本認證(basic auth), bearer token 或者證書認證.需要三個步驟來完成認證配置.

  • 當啟動apiserver時,通過--admission-control-config-file選項來指定準入控制配置文件的位置.

  • 在准入控制配置文件里,指定變換控制器(MutatingAdmissionWebhook)和驗證控制器(ValidatingAdmissionWebhook)從哪裡讀取證書.證書存儲在kubeConfig文件里(和kubectl使用的相同),字段名為kubeConfigFile.下面是准入控制配置文件示例

apiVersion: apiserver.k8s.io/v1alpha1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: <path-to-kubeconfig-file>
- name: MutatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: <path-to-kubeconfig-file>

這裡是admissionConfiguration的schema定義

  • 在kubeConfig文件里,提供證書
apiVersion: v1
kind: Config
users:
# DNS name of webhook service, i.e., <service name>.<namespace>.svc, or the URL
# of the webhook server.
- name: 'webhook1.ns1.svc'
  user:
    client-certificate-data: <pem encoded certificate>
    client-key-data: <pem encoded key>
# The `name` supports using * to wildmatch prefixing segments.
- name: '*.webhook-company.org'
  user:
    password: <password>
    username: <name>
# '*' is the default match.
- name: '*'
  user:
    token: <token>

當然,你需要設置web鈎子服務器來處理這些認證.

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高速輸出-我們戲說緩存

前言

緩存要解決的問題是速度的問題,使用緩存的目的是為了減少對物理資源的訪問,緩存大量的應用在軟硬件的方方面面,從 CPU 到硬盤,就應用了 一級緩存、二級緩存,少部分高速緩存和大量低速緩存相結合,以提高 CPU 的計算能力,本文講的主要是系統集成項目中的軟件級別的緩存。

緩存因果圖

緩存在現代系統中的位置可以說是舉足輕重,不是可有可無的問題了,而是怎麼樣用好的問題。怎麼樣講好緩存這個話題,我思考了很久;如果是生搬硬套,我估計我肯定是涼涼,但要是上來就一頓代碼操作,顯得字太多,各位看官老爺也是要噴死我的(你們噴我算我贏)。各位看官就當故事隨便看看就好了。

客戶端緩存

小明同學是一個大學生,每個月都會從爸爸那裡領取生活費,由於爸爸給的生活費比較充裕,他就將一部分存了起來,周而復始,固定頻率,這就是瀏覽器網頁緩存;有一天,他接到爸爸的一個電話,說每個月給現金,我總是從銀行拿也不方便,這樣,咱給存銀行卡裡頭,但是說好了,每個月1號的時候轉1000塊進去,3號之前可以領取,過時不侯。這就是 Cookie!小明說爸,這時間太緊迫了,我萬一太過於專心學習忘記取錢那你兒子可是要餓死的吖,到時候沒人給你養老送終事兒就大了。他爸爸經過深思熟慮后,決定放開取現時間的限制:行,你想什麼時候取都成,就是別把卡給弄掉了就行。這就是LocalStorage/IndexDB,瀏覽器本地存儲。如果銀行卡掉了(瀏覽器重裝,刪庫跑路),那還得找爸爸。

服務器緩存

我們本次討論的重點就是服務器緩存,對於小明的爸爸來說,每次給小明生活費之前都要到銀行(數據庫)去取錢出來,實在是太麻煩,不如提前把錢取出來準備好,每次取半年的錢出來放家裡保險箱里(緩存),小明申請生活費的時候,直接給他就好了,節約時間效率又高,唯一的問題就是可能沒那麼安全,有可能被小偷把錢給偷了(緩存更新),然後就是半年時間過後再重新取一筆出來(緩存過期策略)。

靜態對象緩存

靜態對象也是一種特殊的緩存,靜態對象作用於程序的整個生命周期中。需要注意的是,靜態對象不會被 GC 回收 ,但是,如果靜態對象被多次引用覆蓋,那麼之前的引用就有可能被 GC 回收。這就好像,小明在向爸爸領取生活費的時候,發現這次領到手的錢實在是太破舊了,都不好意思花出去,爸爸只好重新給了另一份。

會話級緩存

在 Web 站點中,Session 是私有制的,各個 Session 之間是不會共享內存對象的,我們可以利用這個特性(在Asp.NET 時代常用)來暫時保存一些數據,例如用戶購物車。還是以小明的需求為例子,小明下面還有一個妹妹,妹妹每個月都可以從爸爸那裡多領取200元生活費,看到妹妹的資源這麼好,小明憤憤不平,就像妹妹提議共享生活費,一家人一起用,多好吖!結果小明被爸爸揍了一頓,還收回了部分生活費。

進程級別緩存

基本上每個應用程序都具有本地緩存的能力,在 Asp.NET 中就有 MemoryCache ,也叫做進程級緩存(本地緩存),MemoryCache 和 分佈式緩存的作用基本一致,所不同的是,本地緩存在應用程序停止后就會被釋放掉,無法進行持久存儲。就好像,小明在上大學期間,每個月都是生活費的,但是到暑假的時候就沒有了(只能靠苦逼的暑期工掙點生活費啊)。

分佈式緩存

分佈式緩存是個好東西,目前市場上出現了非常多的 nosql 數據庫,都可用作緩存數據庫,有時候,這些緩存數據庫也提供持久化的能力。

小明家的生活條件,在經過爸爸的不懈努力之後,生活水平漸漸提高了;有一天,爸爸對小明說:明仔吖,咱們家現在生活水平提高了,但是爸爸也越來越忙了,這樣,如果我忙的時候,你問媽媽或者爺爺奶奶,也是可以拿到生活費的,咱們家這幾位長輩手裡都有錢了,這就是分佈式緩存。

但是分佈式緩存又分為主從模式和集群模式,上面說的是集群模式,爸爸媽媽爺爺奶奶都可以拿錢,但是主從模式就不同了,主從模式就是錢都在爸爸手裡,爸爸會把錢分給每個長輩一份,如果當時正好爸爸太忙了,沒來得及分給媽媽,而小明又恰好問媽媽要生活費的話;媽媽只能對他說:小命呀,不好意思,媽媽這裏也沒有,你看看再問問其它人(客戶端自己輪詢),在問了媽媽、爺爺后(引用指向),終於知道,錢在爸爸那裡,還得問爸爸要生活費。而且爸爸給其它人分錢的時候,還要佔用他工作的時間。

緩存雪崩

緩存雪崩就是在某一個時刻,大量的緩存同時失效,造成數據庫訪問壓力倍增。小明的爸爸最近壓力比較大,因為收入減少了,他爸爸做的一個工程因為種種原因,貨款只能分期付清,為了防止小明和妹妹同時申請生活費,造成資金周轉困難;爸爸規定妹妹 1 號領取生活費,而小明在 5 號才能領取,小明心裏的苦啊!

緩存穿透

緩存穿透就是客戶端總是嘗試訪問某個不存在的緩存,造成了每次都要取請求數據庫讀取數據。就好像小明吧,本來生活挺平靜的,這剛上大二,就交了個女朋友,每個月的 1000 元生活費有點捉襟見肘,然後他又不能讓爸爸知道,就在申請生活費的時候,每次都多要一點;這樣就搞得爸爸很被動,本來計劃得好好的,每個月都是 1000 塊,這沒次都不夠,老是要跑銀行取現金,終於在3個月後,爸爸發現了這個問題,想著兒子也大了,為了早日抱上孫子,就提高了小明的生活費,解決了每次都要跑銀行的問題。

緩存擊穿

緩存擊穿和緩存雪崩有點類似,其中不同的是;緩存雪崩是大量緩存 key 同時過期,而緩存擊穿是大量的請求指向同一個緩存key,在這個 key 過期的時候,大量的請求湧入數據庫中,造成了瞬間巨大的壓力。舉個栗子,因為小明交了女朋友的原因,他的生活費總是很快用完了;這種情況下,他也必須在 1 號的時候拿到生活費,不然就要吃土了,但是媽媽不允許他們一起取打擾爸爸,媽媽就指定了妹妹去問,在妹妹沒有回來之前,小明只能等着,這就是為了解決緩存擊穿而採用的策略:互斥鎖(mutex key)

運維級別緩存

除了在應用程序中可以接入緩存以外,部分運維工具也集成了緩存服務,比如 Nginx、IIS。

代理緩存

Nginx 就是反向代理緩存,通過配置 Nginx 的緩存功能,在客戶端請求到來到時候去加載緩存內容,用以提高響應能力,IIS 緩存又分為用戶緩存和內核緩存。IIS 的輸出緩存設置中,內核模式緩存不會對驗證等用戶信息進行檢查,就好像小明等爸爸因為太趕時間,把錢放門衛大媽那裡了,結果隨便來了個學生就把小明等生活費給領走了,但是加上用戶模式緩存后,就可以添加對身份的檢查(請求標頭),這樣大媽就會知道誰是小明而不會隨便把生活費交給別人了。

網頁靜態化

這種技術,在 Web1.0 時代非常的流行,我還記得那些個年頭的網站開發項目中的要求,大部分項目的需求分析文檔裏面就明晃晃的寫着:網頁靜態化!,而靜態化常見於各種企業型、論壇帖子,在發表這些信息后就將其生成靜態網頁,客戶端訪問的時候,直接重定向到該靜態網頁,基本無需訪問數據庫。

CDN 緩存

CDN 緩存類似於上面提到的分佈式緩存,但是實際上 CDN 緩存服務目前來說,主要說針對靜態資源的,比如圖片、視頻、文件等等;大家經常可以看到,很多站點都號稱提供了 CDN 加速服務,這些站點就像一個個代辦信用卡的銷售中介,實際上拿的都是銀行的資源。

更形象一些的說法,就是大家的錢都是中國人民銀行發行的,我們可以通過各個不同的銀行(CDN節點)查詢我們的銀行卡餘額(我怎麼可能有餘額),在以前沒有手機銀行的時候,大家就可以到附近的銀行去查詢,然後取款(CDN緩存),如果附近的銀行的櫃員機沒有現金,那麼可能就需要到總行去取了(回源),如果發生了太多回源的事件,就會造成 CDN 的擁堵,所以 CDN 服務商也不敢打包票自己的基礎服務沒有問題,反正我是沒見到哪家 CDN 服務商敢註明服務穩定性 100%,基本上都是 99.99%

結束語

緩存的理論知識,其實是非常宏大的,我這裏只是拋磚引玉,希望能給入門的朋友帶來一點幫助,如果你喜歡這篇文章,請給我點贊,讓更多同學可以看到。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

自定義Starter:

  SpringBoot應用篇(一):自定義starter

一、前言

  上一篇文章,通過分析refresh()方法中的invokeBeanFactoryPostProcessors()方法,分析了IoC容器的初始化過程,這一節從代碼上如下所示,接上一節ConfigurationClassParser類中的parse()方法,接着分析SpringBoot的自動裝配原理。

 1 // ConfigurationClassParser類
 2 public void parse(Set<BeanDefinitionHolder> configCandidates) {
 3     this.deferredImportSelectors = new LinkedList<>();
 4     for (BeanDefinitionHolder holder : configCandidates) {
 5         BeanDefinition bd = holder.getBeanDefinition();
 6         try {
 7             // 如果是SpringBoot項目進來的,bd其實就是前面主類封裝成的 AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的實現類)
 8             if (bd instanceof AnnotatedBeanDefinition) {
 9                 parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
10             } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
11                 parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
12             } else {
13                 parse(bd.getBeanClassName(), holder.getBeanName());
14             }
15         } catch (BeanDefinitionStoreException ex) {
16             throw ex;
17         } catch (Throwable ex) {
18             throw new BeanDefinitionStoreException(
19                     "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
20         }
21     }
22     // 加載默認的配置---》(對springboot項目來說這裏就是自動裝配的入口了)
23     processDeferredImportSelectors();
24 }

 

二、SpringBoot自動裝配原理。

2.1、@SpringBootApplication註解

  對這個註解詳細大家一定非常熟悉了。再來好好看看這個註解。

 1 @Target(ElementType.TYPE)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 @Inherited
 5 @SpringBootConfiguration
 6 @EnableAutoConfiguration
 7 @ComponentScan(excludeFilters = {
 8         @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
 9         @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
10 public @interface SpringBootApplication {
11     ...
12 }

  接着看@EnableAutoConfiguration

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @AutoConfigurationPackage
6 @Import(AutoConfigurationImportSelector.class)
7 public @interface EnableAutoConfiguration {
8     ...
9 }

  OK,看到@Import(AutoConfigurationImportSelector.class)導入了一個重要的類AutoConfigurationImportSelector。

2.2、AutoConfigurationImportSelector

 1 // AutoConfigurationImportSelector類
 2 //自動裝配
 3 @Override
 4 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 5     if (!isEnabled(annotationMetadata)) {
 6         return NO_IMPORTS;
 7     }
 8     AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 9             .loadMetadata(this.beanClassLoader);
10     AnnotationAttributes attributes = getAttributes(annotationMetadata);
11     //獲取所有的自動配置類(META-INF/spring.factories中配置的key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的類)
12     List<String> configurations = getCandidateConfigurations(annotationMetadata,
13             attributes);
14     configurations = removeDuplicates(configurations);
15     //需要排除的自動裝配類(springboot的主類上 @SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})指定的排除的自動裝配類)
16     Set<String> exclusions = getExclusions(annotationMetadata, attributes);
17     checkExcludedClasses(configurations, exclusions);
18     //將需要排除的類從 configurations remove掉
19     configurations.removeAll(exclusions);
20     configurations = filter(configurations, autoConfigurationMetadata);
21     fireAutoConfigurationImportEvents(configurations, exclusions);
22     return StringUtils.toStringArray(configurations);
23 }

  至於怎麼從章節一中提到的ConfigurationClassParser類中的parse()===>processDeferredImportSelectors()==>AutoConfigurationImportSelector#selectImports(),篇幅有限不做過多介紹。

   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 

  我們來看一下getCandidateConfigurations()方法是怎麼拿到這些自動配置類的。

// AutoConfigurationImportSelector類
1
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, 2 AnnotationAttributes attributes) { 3 List<String> configurations = SpringFactoriesLoader.loadFactoryNames( 4 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); 5 Assert.notEmpty(configurations, 6 "No auto configuration classes found in META-INF/spring.factories. If you " 7 + "are using a custom packaging, make sure that file is correct."); 8 return configurations; 9 }

  是不是又看到一個十分熟悉的方法loadFactoryNames(),沒錯,其實我們在分析SpringBoot啟動流程的第一篇文章的時候,就已經分析了,SpringBoot是如何從META-INF/spring.factories中加載指定key的value的。ok,我們在這裏再次回顧一遍。

  看看loadFactoryNames()方法

// SpringFactoriesLoader類
1
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { 2 String factoryClassName = factoryClass.getName(); 3 return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); 4 }

  debug,看看要從META-INF/spring.factories中加載的類的key,如下圖所示:org.springframework.boot.autoconfigure.EnableAutoConfiguration

  回到selectImports()方法,debug,跳過List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);看一下configurations

  竟然有110個,那這些類都在哪裡呢?看spring-boot-autoconfigure(當然在SpringBoot的工程中,也不止這一個依賴包中存在該配置文件)工程下的META-INF/spring.factories,我們能看到org.springframework.boot.autoconfigure.EnableAutoConfiguration定義了一大堆。

  其中還有一個com.demo.starter.config.DemoConfig是我自定義的starter。如下所示,我在測試工程中添加了自定義starter的依賴,所以SpringBoot就能掃描到。

1 <dependency>
2     <groupId>com.demo</groupId>
3     <artifactId>demo-spring-boot-starter</artifactId>
4     <version>0.0.1-RELEASE</version>
5 </dependency>

   繼續看Set<String> exclusions = getExclusions(annotationMetadata, attributes);方法,該方法是排除主類上@SpringBootApplication註解上排除的自動裝配的類。比如我們在該註解上排除我們自定義starter的自動裝配的類,@SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})(當然也可以用excludeName進行排除),那麼在後面的configurations.removeAll(exclusions);方法中將會刪除我們的com.demo.starter.config.DemoConfig.class。

  configurations = filter(configurations, autoConfigurationMetadata);該行代碼將會過濾掉不需要裝配的類。過濾的邏輯有很多,比如我們常用的@ConditionXXX註解。如下所示:

 1 @ConditionalOnBean:容器中有指定的Bean 
 2 @ConditionalOnClass:當類路徑下有指定的類
 3 @ConditionalOnExpression:基於SpEL表達式作為判斷條件  
 4 @ConditionalOnJava:基於JVM版本作為判斷條件  
 5 @ConditionalOnJndi:在JNDI存在的條件下查找指定的位置  
 6 @ConditionalOnMissingBean:當容器中沒有指定Bean的情況下  
 7 @ConditionalOnMissingClass:當類路徑下沒有指定的類
 8 @ConditionalOnNotWebApplication:當前項目不是Web項目
 9 @ConditionalOnProperty:配置文件中指定的屬性是否有指定的值  
10 @ConditionalOnResource:類路徑下是否有指定的資源  
11 @ConditionalOnSingleCandidate:當指定Bean在容器中只有一個,或者雖然有多個但是指定首選Bean
12 @ConditionalOnWebApplication:當前項目是Web項目的條件下

   至於如何將這些類解析成BeanDefinition並註冊進beanDefinition中的,和上一節講的過程是一樣的,不再贅述了。

  debug,跳過refresh()方法中的invokeBeanFactoryPostProcessors(beanFactory);方法。如下圖所示,最終在beanFactory的BeanDefinitionMap中找到了自定義starter中的自動裝配的類。

 

  綜合本文和上一篇博文我們詳細的梳理了IoC容器的初始化過程,到此IoC容器的初始化過程就結束了。

 

  

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

一文帶你了解爬蟲

六月分享主題:爬蟲
HTTP詳解
網頁結構簡介

前段時間我媽突然問我:兒子,爬蟲是什麼?我當時既驚訝又尷尬,驚訝的是為什麼我媽會對爬蟲好奇?尷尬的是我該怎麼給她解釋呢?

一、爬蟲介紹

1.爬蟲是什麼

網絡爬蟲(web crawler 簡稱爬蟲)就是按照一定規則從互聯網上抓取信息的程序,既然是程序那和正常用戶訪問頁面有何區別?爬蟲與用戶正常訪問信息的區別就在於:用戶是緩慢、少量的獲取信息,而爬蟲是大量的獲取信息。

這裏還需要注意的是:爬蟲並不是Python語言的專利,Java、Js、C、PHP、Shell、Ruby等等語言都可以實現,那為什麼Python爬蟲會這麼火?我覺得相比其他語言做爬蟲Python可能就是各種庫完善點、上手簡單大家都在用,社區自然活躍,而社區活躍促成Python爬蟲慢慢變成熟,成熟又促使更多用戶來使用,如此良性循環,所以Python爬蟲相比其他語言的爬蟲才更火。

下面是一段hello world級別的Python爬蟲,它等效於你在百度搜索關鍵字:python

2.爬蟲案例

既然爬蟲是大量抓取網頁,那是不是爬蟲都是不好的呢?答案當然不是,可以說我們的日常上網已經離不開爬蟲了,為什麼這麼說?下面我就為大家盤點幾個爬蟲日常應用:

  1. 搜索引擎:如Google、百度、雅虎、搜狗、必應等等很多搜索引擎其本質就是一個(可能多個)巨大爬蟲,這些搜索引擎工作原理是:頁面收錄->頁面分析->頁面排序->響應關鍵字查詢,也就是說它會先把互聯網上很多頁面保存到服務器,然後分析網頁內容建立關鍵字索引,最後用戶輸入關鍵字的時候去查詢內容,然後根據相關性排序(百度害人的競價排名毫無相關性可言),第一步的頁面收錄就是爬蟲,百度查看一個網站有多少網頁被收錄方法,百度輸入:site:你想查詢的網站,如:site:blog.csdn.net。
  2. 搶票軟件:很多人在吐槽12306卡,可你不知道12306幾乎每天都相當於淘寶雙11的流量,這誰受得了。為什麼每天都是如此高的流量?答案自然是爬蟲了,為什麼搶票軟件可以搶票?因為它在不斷的刷新和監控是否有餘票,大大小小這麼多搶票app,訪問量之高可想而知。之前很多公司都出過搶票插件,如:百度、360、金山、搜狗等等,後來都被鐵道部約談下線了,而現在又流行搶票app,為什麼搶票app可以,插件就不可以?可能是因為管理和可控性的原因。
  3. 惠惠購物助手:這是一款能進行多個網站比價並能知道最低價的一個網站,其工作原理也是通過大量爬蟲爬取商品價格然後存儲,這樣就可以製作一個價格走勢圖,幫助你了解商品最低價。

二、爬蟲的價值

從上面舉的幾個例子來看,爬蟲對整個互聯網的價值真的無法估量,那對於小我而言,爬蟲能給我們帶來哪些價值?

1.隱形的翅膀

如果你問我學完Python基礎之後該學習什麼技能?我會毫不猶疑的說爬蟲,為什麼是爬蟲?

  1. 爬蟲相對其他技能簡單易學,且效果立即可見,會有一定的成就感
  2. 爬蟲可以說是其他技能的基石,因為他是數據的來源,現在這個時代誰有數據誰才能稱王,所以會爬蟲絕對會讓你如虎添翼
  3. 在國內,很多企業巴不得你啥都會,所以在應聘時,爬蟲會是一個不錯的加分項

2.看不見的商戰

職場對話:

老闆:小明給你個重要任務。
小明:就算996我也在所不辭(第一次接到老闆的直接需求)!
老闆:你能不能搞到競爭對手的商品價格?
小明:沒問題(牛逼先吹出去),小事!
老闆:這可不是小事,只要你能持續獲取競品價格,我們就可以標價低於他們一點點,持續如此大家就知道我們的價格肯定是比他們低,這樣大家就直接來我們這裏購買商品了,到時候慶功宴上你就是最大功臣(先畫個餅)。
小明:老闆牛逼,老闆英明!

3.會爬蟲就可以創業

工作之後很多同學會利用上班空餘時間,來做自己的東西或者項目,別看開始是一個小打小鬧的東西,慢慢豐富將來也許會成為一款成熟的產品。

而爬蟲可以讓你很輕鬆的實現自己的產品,做的好的話就可以創業。這裏豬哥給大家列幾個簡單的創業項目,當然作為一個思維引導。

如果你想做一款好的產品,你需要從用戶需求出發去思考,做解決目前存在的問題的產品或者目前還沒有的服務,也許你的產品就是下一個頭條。

三、盜亦有道

爬蟲既然如此強大而優秀,那是不是會了爬蟲就可以為所欲為?

延伸話題:其實我內心一直有一個疑問:為什麼互聯網公司都偏愛用動植物來命名或作為logo?如:螞蟻金服、天貓、菜鳥、金東狗、騰訊企鵝,百度的熊掌、搜狗、途牛、美團的袋鼠。。。真的是太多了,難道僅僅是因為好記?我認為好記是一個原因,其根本原因是受到編程行業的影響,你想想編程行業有多少動植物:Java(咖啡)、Python(蟒蛇)、Go(囊地鼠)、PHP(大象)、Linux(企鵝)、Perl(駱駝)、Mysql(海豚)等等,具體為什麼編程行業喜歡用動植物這豬哥就不清楚,還請明白的同學留言告知大家!

講上面的延伸話題想表達的是,大自然世間萬物,相生相剋,衡而不禍!而爬蟲也是如此,下面就為大家介紹一下約束爬蟲的幾個點。

1.robots協議

做過網站的同學也許知道,我們在建站的時候需要在網站根目錄下放一個文件:robots.txt,這個文件是干什麼用的呢?

Robots協議,也稱為爬蟲協議、機器人協議等,其全稱為“網絡爬蟲排除標準(Robots Exclusion Protocol)”。網站通過Robots協議告訴搜索引擎哪些頁面可以抓取,哪些頁面不能抓取。

每個網站的根目錄下都必須放置robots.txt文件,否則搜索引擎將不收錄網站任何網頁。

下面我們以百度為例子,來看看百度的robots.txt文件:

我們在百度robots協議的最下面,有這樣一條:

User-agent: *
Disallow: /

這表示除上面那些定義過的爬蟲以外其他爬蟲都不允許爬取百度任何東西!

2.法律

我們都知道在發起請求時User-agent是可以自定義的,也就是說我們可以繞過robots協議和以User-agent來定義的反爬蟲技術,所以robots協議可能更像一個君子協議,那咱們國家有沒有法律明文規定呢?我們來了解一下非法侵入計算機信息系統罪

第二百八十五條 非法侵入計算機信息系統罪:違反國家規定,侵入前款規定以外的計算機信息系統或者採用其他技術手段,獲取該計算機信息系統中存儲、處理或者傳輸的數據,或者對該計算機信息系統實施非法控制,情節嚴重的,處三年以下有期徒刑或者拘役,並處或者單處罰金;情節特別嚴重的,處三年以上七年以下有期徒刑,並處罰金。

我們可以看到重點信息:入侵計算機獲取數據是違法的,也就是說爬蟲技術本身是無罪的,因為它是獲取的公開信息,並未非法入侵計算機。但是如果你用爬取到的數據去從事商業化操作,那也許就構成了違法犯罪行為!

給大家介紹下因爬蟲而被判刑的案件:

  1. 頭條爬蟲官司:https://dwz.cn/qBBHc2fq
  2. 酷米客公司官司:https://dwz.cn/9IpAOHGB
  3. 360和百度爬蟲官司:https://dwz.cn/RGRa3HJc

我發現這幾起案件的通性有兩點:1、公司性質。2、競爭對手。3、讓人家找到線索。

最後提醒大家:技術人,要守住自己的底線,違反國家法律法規的事情咱們堅決不能做!

3.反爬蟲工程師

本來想採訪一位攜程反爬蟲工程師,但是他說由於工作保密原因不便接受採訪,所以只好尊重他的選擇!

四、爬蟲現狀

之前豬哥說過互聯網中有超過50%的流量來自爬蟲,那我們就來簡單聊聊目前爬蟲的現狀吧!

1.技術

反爬蟲幾乎和爬蟲同時誕生,它們是相愛相殺的連個技術,如果沒有爬蟲就沒有反扒,而反扒技術又反過來又可以促進爬蟲技術的發展。

  1. 交互問題:各種變態的驗證碼充斥,尤其是12306,分分鐘都想爆粗口,以後會越來越變態。。。
  2. js加密:最近很流行的一種反扒技術,會爬蟲還得先學js,然後反爬蟲工程師在js裏面各種投毒,殺人誅心。。。
  3. IP限制:限制單個ip某段時間內訪問次數

豬哥只介紹了一個反扒技術,當然都有現成的技術解決,但是作為爬蟲員最重的並不是會使用工具或框架去應對反扒,而是能通過自己的思考和摸索破解反扒,因為反扒技術更新快且多樣化。

2.就業

就業的話主要從招聘、薪資、就業形勢三個方面分析,我從網上查閱了一些資料,然後整理以圖片形式提供給大家參考。

數據來源:職友集:https://dwz.cn/6PeU46QY

3.前景

現在很多人並不看好爬蟲的前景,只搞爬蟲的話技術只停留在當前水平,不再學習新知識,不再進步的話,那麼是沒有前途的,總有一天會被時代淘汰,其實其他職位也只如此。

每個職業都是有一個橫向和縱向的發展,也就是所謂的廣度和深度的意思。第一、如果專研得夠深,你的爬蟲功能很強大,性能很高,擴展性很好等等,那麼還是很有前途的。第二、爬蟲作為數據的來源,後面還有很多方向可以發展,比如可以往大數據分析、數據展示、機器學習等方面發展,前途不可限量,現在作為大數據時代,你佔據在數據的的入口,還怕找不到發展方向?所以爬蟲也許只是一個起點一個跳板,是你走向人生巔峰的一個基石,總有一天你會迎娶白富美!

五、總結

本期為大家講解什麼是爬蟲、爬蟲的價值、爬蟲的合法性以及爬蟲的現狀,下期豬哥將採訪一位牛逼的爬蟲工程師,讓他為大家講講實際工作中的一些知識以及作為一名爬蟲工程師該掌握和學習哪些技能,讓大家能更近距離接觸爬蟲,如果你有想對他提問的問題,可以在下方留言哦,我會抽取一些問題讓他為大家解答,問題不限

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Spring Boot從入門到實戰:整合通用Mapper簡化單表操作

數據庫訪問是web應用必不可少的部分。現今最常用的數據庫ORM框架有Hibernate與Mybatis,Hibernate貌似在傳統IT企業用的較多,而Mybatis則在互聯網企業應用較多。通用Mapper(https://github.com/abel533/Mapper) 是一個基於Mybatis,將單表的增刪改查通過通用方法實現,來減少SQL編寫的開源框架,且也有對應開源的mapper-spring-boot-starter提供。我們在此基礎上加了一些定製化的內容,以便達到更大程度的復用。

框架源碼地址:https://github.com/ronwxy/base-spring-boot (持續更新完善中,歡迎follow,star)
Demo源碼地址:https://github.com/ronwxy/springboot-demos/tree/master/springboot-tkmapper

在開源mapper-spring-boot-starter的基礎上,增加了如下內容:

  1. 針對MySQL數據庫與PostgreSQL數據庫添加了一些Java類型與數據庫類型的轉換處理類,如將List、Map類型與MySQL數據庫的json類型進行轉換處理
  2. 對Domain、Mapper、Service、Controller各層進行了封裝,將基本的增刪改查功能在各層通用化
  3. 提供了基於druid連接池的自動配置
  4. 其它一些調整,如默認映射複雜類型屬性(主要是List、Map類型,其它自定義類型需要自定義轉換處理類),將枚舉作為簡單類型處理
  5. 提供了一個parent項目,將一些常用的框架進行集成,實際項目可繼承parent簡化依賴配置(持續更新完善)

該框架可用於實際基於springboot的項目,只需簡單配置數據源,即可引入druid連接池及通用mapper的功能,以及各層基本的增刪改查方法。

如何使用?
下文給出使用步驟,可參考示例:https://github.com/ronwxy/springboot-demos/tree/master/springboot-tkmapper

1. 框架Maven部署安裝

下載框架源碼后,在項目根路徑下執行mvn clean install可安裝到本地maven庫。如果需要共享,且搭了Nexus私服,則在根路徑pom.xml文件中添加distributionManagement配置,指定Nexus倉庫分發地址,使用mvn clean deploy安裝到遠程maven倉庫,如

<distributionManagement>
     <repository>
         <id>nexus-releases</id>
         <url>
             http://ip:port/repository/maven-releases/
         </url>
     </repository>
     <snapshotRepository>
         <id>nexus-snapshots</id>
         <url>
             http://ip:port/repository/maven-snapshots/
         </url>
     </snapshotRepository>
 </distributionManagement>

 

上述指定的repository需要在maven的全部配置文件settings.xml中有對應賬號配置(id需要一一對應),如 

 <servers>
   <server>
     <id>nexus-snapshots</id>
     <username>admin</username>
     <password>xxx</password>
   </server>
<server>
     <id>nexus-releases</id>
     <username>admin</username>
     <password>xxx</password>
   </server>
 </servers>

 

2. pom.xml配置

項目中引入該數據庫框架有三種方式:

  1. 直接引入 cn.jboost.springboot:tkmapper-spring-boot-starter(沒有連接池)
  2. 直接引入 cn.jboost.springboot:druid-spring-boot-starter(druid連接池支持)
  3. 項目繼承 cn.jboost.springboot:spring-boot-parent(使用的是druid連接池)

三種方式的pom.xml配置如下

#第一種方式
<dependency>
   <groupId>cn.jboost.springboot</groupId>
   <artifactId>tkmapper-spring-boot-starter</artifactId>
   <version>1.2-SNAPSHOT</version>
</dependency>

#第二種方式
<dependency>
   <groupId>cn.jboost.springboot</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2-SNAPSHOT</version>
</dependency>

#第三種方式
<parent>
   <groupId>cn.jboost.springboot</groupId>
   <artifactId>spring-boot-parent</artifactId>
   <version>1.2-SNAPSHOT</version>
   <relativePath/> <!-- lookup parent from repository -->
</parent>

 

根據情況引入mysql或postgresql的驅動依賴(其它數據庫暫未做類型轉換支持,未作測試)

 

3. 配置數據源

如果使用druid連接池,則在application.yml配置文件中,加入如下數據源配置(推薦)

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
      username: root
      password:
      # 自定義配置
      initialSize: 2  # 初始化大小
      minIdle: 1   # 最小連接
      maxActive: 5 # 最大連接
      druidServletSettings:
        allow: 127.0.0.1
        deny:
        loginUsername: admin
        loginPassword: Passw0rd
        resetEnable: true
      druidFilterSettings:
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
      maxWait: 60000   # 配置獲取連接等待超時的時間
      timeBetweenEvictionRunsMillis: 60000 # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
      minEvictableIdleTimeMillis: 300000 # 配置一個連接在池中最小生存的時間,單位是毫秒
      validationQuery: SELECT 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true # 打開PSCache,並且指定每個連接上PSCache的大小
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat #,wall(添加wall代碼里不能直接拼接sql,druid有sql注入校驗) # 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
      useGlobalDataSourceStat: true # 合併多個DruidDataSource的監控數據

 

如果不使用連接池,則配置相對簡單,如下 

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
    username: root
    password:
    driver-class-name: com.mysql.jdbc.Driver

 

4. 定義相應domain,mapper,service,controller各層對象 

以demo為例(demo數據庫腳本見resources/schema.sql),domain定義一個User類,

@Table(name = "user")
@Getter
@Setter
@ToString
public class User extends AutoIncrementKeyBaseDomain<Integer> {
    private String name;
    @ColumnType(jdbcType = JdbcType.CHAR)
    private Gender gender;
    private List<String> favor;
    private Map<String, String> address;

    public enum Gender{
        M,
        F
    }
}

 

需要添加@Table註解指定數據庫表名,可通過繼承AutoIncrementKeyBaseDomain來實現自增主鍵,或UUIDKeyBaseDomain來實現UUID主鍵,如果自定義其它類型主鍵,則繼承BaseDomain。 

該框架Service層通用方法實現BaseService只支持單列主鍵,不支持組合主鍵(也不建議使用組合主鍵)

框架默認對List、Map等複雜類型屬性會映射到mysql的json類型或postgresql的jsonb類型,如果某個屬性不需要映射,可添加@Transient註解;枚舉類型需添加@ColumnType指定jdbcType。

dao層定義UserMapper

@Repository
public interface UserMapper extends BaseMapper<User> {
}

 

BaseMapper默認實現了單表的增刪改查及批量插入等功能,如需定義複雜查詢,可在該接口中定義,然後通過mapper xml文件編寫實現。 

service層定義 UserService,繼承了BaseService的通用功能(具體可查看源碼),同樣可在該類中自定義方法

@Service
public class UserService extends BaseService<Integer, User> {

    @Transactional
    public void createWithTransaction(User user){
        create(user);
        //用於測試事務
        throw new RuntimeException("拋出異常,讓前面的數據庫操作回滾");
    }
}

 

controller層定義 UserController,繼承了BaseController的通用接口(具體可查看源碼) 

@RestController
@RequestMapping("/user")
public class UserController extends BaseController<Integer, User> {
}

 

如上,只需要定義各層對應的接口或類,繼承基礎接口或類,便完成了用戶基本的增刪改查功能,不需要寫一行具體的實現代碼。 

5. 測試、運行

  1. 示例中提供了兩個新建用戶的單元測試,參考SpringbootTkmapperApplicationTests

  2. 運行,在主類上直接運行,然後瀏覽器里打開 http://localhost:8080/user 則可列出單元測試中創建的用戶(其它接口參考BaseController實現)

6. 總結

本文介紹框架基於tk.mybatis:mapper-spring-boot-starter做了一些自定義擴展,以更大程度地實現復用。可用於實際項目開發,使用過程中如果遇到問題,可關注公眾號留言反饋。

我的個人博客地址:http://blog.jboost.cn
我的頭條空間: https://www.toutiao.com/c/user/5833678517/#mid=1636101215791112
我的github地址:https://github.com/ronwxy
我的微信公眾號:jboost-ksxy

————————————————————————————————————————

歡迎關注我的微信公眾號,及時獲取最新分享

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益