自定義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網頁設計已成為網頁設計推薦首選

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

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

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

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網頁設計已成為網頁設計推薦首選

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

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

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

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網頁設計已成為網頁設計推薦首選

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

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

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

一文帶你了解爬蟲

六月分享主題:爬蟲
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網頁設計已成為網頁設計推薦首選

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

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

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

Trie|如何用字典樹實現搜索引擎的關鍵詞提示功能

Trie字典樹

Trie字典樹又稱前綴樹,顧名思義,是查詢前綴匹配的一種樹形數據結構

可以分為插入(創建) 和 查詢兩部分。參考地址極客時間

下圖為插入字符串的過程:

創建完成后,每個字符串最後一個字母標記為終結點(圖中显示為紅色)

下圖為查詢字符串:“her”的過程:綠色箭頭表示查詢路徑
我們將要查找的字符串分割成單個的字符 h,e,r,一個一個查詢

下圖為查詢字符串:“he”的過程:綠色箭頭表示查詢路徑
因為‘e’不是終結點,所以不能完全匹配上。

Trie字典樹的實現

1.首先是字典樹 數據結構定義的代碼實現

樹形結構,類比於二叉樹的存儲嘛,每個結點兩條分支(二叉樹);
而字典樹,每個節點可以最多有 26個分支(存儲英文字母)。

1-1二維數組存儲字母

int trie[MAX_NODE][26];//MAX_NODE表示結點數量,每個結點有26個字母結點
int k;

MAX_NODE表示結點數量,每個結點有26個字母結點
Trie[i][j]的值是0,表示trie樹中i號節點,並沒有一條連出去的邊滿足邊上的字符標識是字符集中第j個字符(從0開始);
trie[i][j]的值是正整數x表示trie樹中i號節點,有一條連出去的邊滿足邊上的字符標識是字符集中第j個字符,並且
這條邊的終點是x號節點。

1-2鏈表
我這裏用C++中的vector實現,

vector< pair<char, int> > trie[MAX_NODE];
int k;

也可以寫一個真正的鏈表,包含二元組字段<char,int>型的對應關係

1-3hash,

map<char, int> trie[MAX_NODE];

每次我們想找i號節點有沒有標識
是某個字符ch的邊時,只要看trie[i][ch]的值即可
但是實際上map時空複雜度的常數都比較大

2.插入 和 查詢 兩個函數的代碼實現

插入 查詢 實際上是類似的,就是從樹的根開始往下遍歷,

2-1插入:從樹的根開始往下遍歷,到達一個結點,沒有這個字母就插入到這個結點下,作為這個結點的子節點

基於二維數組結構的插入功能實現

代碼的第6~8行,一開始trie[][]被初始化為0,保證每個節點被創建出來時,都沒有子節點。K初
始化為1表示一開始只有1個節點,也就是0號節點根節點。Color是用來標記一個節點是不是終結
點。Color[i]=1標識i號節點是終結點。
第9~21行是插入函數insert(w),w是字符指針,實際上可以看作是一個字符串。
第11行是p從0號節點開始。
第12~19行是依次插入w的每一個字符。
第13行是計算w[i]是字符集第幾個字符,這裏我們假設字符集只包含26個小寫字母。
第14~17行是如果p沒有連出標識是w[i]的邊,那麼就創建一個。這裏新創建的節點一定就是k號節
點。所謂創建新節點實際上也沒什麼可創建的,新節點就是個編號。所以我們直接令trie[i][c]=k
即可,然後將k累加1,整個創建過程就完成了。
第18行是沿着標記着w[i]的邊移動到下一個節點。
最後第20行,是將最後到達的節點p標記為終結點。

2-2查詢:從樹的根開始往下遍歷,查看是否匹配上當前正在查的單詞
基於二維數組結構的查詢功能實現

第24行是從p=0也就是根節點開始。
第25~29行是枚舉s的每一個字符。
第26行是計算當前字符s[i]在字符集的序號。
第27行是判斷p節點有沒有連出標識s[i]字符的邊,如果沒有,說明現在無路可走,直接返回0;如
果有的話,
第28行就是移動到下一個節點。如果整個循環結束還沒有return 0,那就說明成功沿着s的每一個
字符到達了p節點。這時只要判斷p節點是不是終結點即可,也就是第30行的代

3.完整代碼C++版

public class Trie {
  private TrieNode root = new TrieNode('/'); // 存儲無意義字符

  // 往 Trie 樹中插入一個字符串
  public void insert(char[] text) {
    TrieNode p = root;
    for (int i = 0; i < text.length; ++i) {
      int index = text[i] - 'a';
      if (p.children[index] == null) {
        TrieNode newNode = new TrieNode(text[i]);
        p.children[index] = newNode;
      }
      p = p.children[index];
    }
    p.isEndingChar = true;
  }

  // 在 Trie 樹中查找一個字符串
  public boolean find(char[] pattern) {
    TrieNode p = root;
    for (int i = 0; i < pattern.length; ++i) {
      int index = pattern[i] - 'a';
      if (p.children[index] == null) {
        return false; // 不存在 pattern
      }
      p = p.children[index];
    }
    if (p.isEndingChar == false) return false; // 不能完全匹配,只是前綴
    else return true; // 找到 pattern
  }

  public class TrieNode {
    public char data;
    public TrieNode[] children = new TrieNode[26];
    public boolean isEndingChar = false;
    public TrieNode(char data) {
      this.data = data;
    }
  }
}

Trie字典樹的時間複雜度 與 缺點

插入的時間複雜度:O(N),N為所有待插入字符串的長度之和
查詢的時間複雜度:O(K),K為待查詢字符串的長度

占內存:如果用二維數組實現,每個節點就會額外需要 26*8=208 個字節
優化思路:將每個節點中的數組換成其他數據結構,比如有序數組(可以二分查找)、跳錶、散列表、紅黑樹等。

Trie變體,縮點優化:對只有一個子節點的節點,而且此節點不是一個串的結束節點,可以將此節點與子節點合併

Trie字典樹的實際應用

1.搜索引擎輸入框關鍵詞提示

因為字典樹是查找 “與前綴匹配的字符串”,又稱為前綴樹。
關鍵詞提示就是 查尋找前綴匹配的前綴合適關鍵詞,當然還有更複雜的關鍵詞排名問題,這裏不再展開。

2.自動補全功能,如:IDE編譯器自動補全,輸入法自動補全等

原理與搜索引擎類似。

3.敏感詞過濾系統

4.其它

Trie在面試與算法競賽中的例題

1.hihoCoder1014

hihoCoder1014

解題思路:Trie字典樹

首先我們把集合中的N個字符串都插入到trie中。
對於每一個查詢s我們在trie中查找s,如果查找過程中無路可走,那麼一定沒有以s為前綴的字符串。
如果最後停在一個節點p,那我們就要看看以p為根的子樹里一共有多少終結點。
終結點的數目就是答案。

但是如果我們每次都遍歷以P為根的子樹,那時間複雜度就太高了。解決的辦法是用空間換時間,我們增加一個數組intcnt[MAX_NODE]
cnt[i]記錄的是以i號節點為根的子樹中,有幾個終結點。
然後我們每次insert一個字符串的時候,順便就把沿途的節點的cnt值都+1。
這樣就不用每次遍歷以P為根的子樹,而是直接輸出cnt[P]即可。

代碼:

2.hihoCoder1107微軟面試題

hihoCoder1014

其實就是找一個節點p,滿足以p為根的子樹中的終結點不多於5個,同時以p的父節點為根的子樹中的終結點大於5個。
和上題一樣用cnt數組標記,之後dfs查找終結點的數目

3.Trie應用在整數xor異或值最大的題目

給定一個包含N個整數的集合S={A1, A2, A3, … AN}。然
後有M個詢問,每次詢問給定一個整數X,讓你找一個Ai使得Ai xor X的值最大。

首先我們知道一個整數可以用二進製表示成一個01串。比如3=(011)2, 5=(101)2, 4=(100)2……。
我們假設輸入的整數都在0~2^32-1之間,於是我們可以用一個長度是32位的01串表示一個整數。
然後對於給定的N個整數A1, A2, A3, … AN,我們把它們對應的01串都插入到一個trie中。注意這裏字符集只有0和1,所以整個trie是一棵二叉樹。

下面我們舉一個例子,為了描述方便,我們假設整數都在0~7之間,也就是可以用3位01串表示。
現在假設S={1, 2, 7},也就是說我們要在Trie中插入{001, 010, 111}:

這時假設我們要查詢x=4,也就是哪個數和4異或結果最大?4=(100)2,
我們的做法是在trie樹中,盡量與4的二進制位反着走。
比如4的第一位(最高位)是1,我們從0出發第一步就盡量沿着0走。因為我們要異或和最大,01相反才能異或值是1。
並且這一步是可以貪心的,也就是說如果有相反的邊,那麼我們一定沿着這條邊走。因為最高位異或得1的話,即便後面都是0, 10000…000也要比最高位是0,後面都是1的011111…111大。
所以我們第一步沿着標識是0的邊,移動到了1號節點;4第二位是0,所以我們沿着標識是1的邊移動到4號節點;
4的第三位是0,但是4號節點沒有標識是1的邊,所以我們也只好沿着標識是0的邊移動到5號節點。
已經到了終結點,所以5號節點對應的A2=(010)2=2就是我們要求的答案,A2 xor 4 = 6是最大的。

【精選推薦文章】

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

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

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

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

ApplicationContextRunner如何簡化自動配置測試

 

1. 概覽

眾所周知,自動配置是Spring Boot的關鍵功能之一, 但測試自動配置可能會很棘手。

在以下部分中,我們將展示ApplicationContextRunner如何簡化自動配置測試。

2. 測試自動化配置方案

ApplicationContextRunner是一個實用程序類,它運行ApplicationContext並提供AssertJ樣式斷言。 最好用作測試類中的字段以便共享配置,然後我們在每個測試中進行自定義:

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

讓我們通過測試一些案例來展示它的魔力。

2.1. 測試Class Condition

在本節中,我們將測試一些使用@ConditionalOnClass和@ConditionalOnMissingClass 註解的自動配置類:

@Configuration
@ConditionalOnClass(ConditionalOnClassIntegrationTest.class)
protected static class ConditionalOnClassConfiguration {
    @Bean
    public String created() { return "This is created when ConditionalOnClassIntegrationTest is present on the classpath"; } } @Configuration @ConditionalOnMissingClass("com.baeldung.autoconfiguration.ConditionalOnClassIntegrationTest") protected static class ConditionalOnMissingClassConfiguration { @Bean public String missed() { return "This is missed when ConditionalOnClassIntegrationTest is present on the classpath"; } } 

我們想測試自動配置是否正確實例化或跳過createdmissing beans給定的預期條件。

  • ApplicationContextRunner為我們提供了withUserConfiguration方法,我們可以根據需要提供自動配置,以便為每個測試自定義ApplicationContext

  • run 方法將 ContextConsumer 作為將斷言應用於上下文的參數。 測試退出時,ApplicationContext將自動關閉:

@Test
public void whenDependentClassIsPresent_thenBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)       .run(context -> {         assertThat(context).hasBean("created");         assertThat(context.getBean("created"))           .isEqualTo("This is created when ConditionalOnClassIntegrationTest is present on the classpath");       }); }   @Test public void whenDependentClassIsPresent_thenBeanMissing() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)         .run(context -> {             assertThat(context).doesNotHaveBean("missed");         }); } 

通過前面的示例,我們發現測試classpath上存在某個類的場景的簡單性。但是,當類不在classpath上時,我們如何測試相反的情況呢

這就是FilteredClassLoader發揮作用的地方。它用於在運行時過濾classpath上指定的類:

@Test
public void whenDependentClassIsNotPresent_thenBeanMissing() {     this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)         .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))         .run((context) -> {             assertThat(context).doesNotHaveBean("created");             assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);         }); }   @Test public void whenDependentClassIsNotPresent_thenBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)       .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))       .run((context) -> {         assertThat(context).hasBean("missed");         assertThat(context).getBean("missed")           .isEqualTo("This is missed when ConditionalOnClassIntegrationTest is present on the classpath");         assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);       }); } 

2.2. 測試 Bean Condition

我們剛剛測試了 @ConditionalOnClass 和 @ConditionalOnMissingClass 註解, 現在 讓我們看看使用@ConditionalOnBean和@ConditionalOnMissingBean註釋時的情況。

首先, 我們同樣需要 一些自動配置的類:

@Configuration
protected static class BasicConfiguration {
    @Bean
    public String created() {         return "This is always created";     } } @Configuration @ConditionalOnBean(name = "created") protected static class ConditionalOnBeanConfiguration {     @Bean     public String createOnBean() {         return "This is created when bean (name=created) is present";     } } @Configuration @ConditionalOnMissingBean(name = "created") protected static class ConditionalOnMissingBeanConfiguration {     @Bean     public String createOnMissingBean() {         return "This is created when bean (name=created) is missing";     } } 

然後,我們將像上一節一樣調用withUserConfiguration方法,然後發送我們的自定義配置類來測試自動配置是否在不同的條件下恰當地實例化bean或跳過createOnBeancreateOnMissingBean :

@Test
public void whenDependentBeanIsPresent_thenConditionalBeanCreated() {     this.contextRunner.withUserConfiguration(BasicConfiguration.class,       ConditionalOnBeanConfiguration.class)     // ommitted for brevity } @Test public void whenDependentBeanIsNotPresent_thenConditionalMissingBeanCreated() {     this.contextRunner.withUserConfiguration(ConditionalOnMissingBeanConfiguration.class)     // ommitted for brevity } 

2.3. 測試 Property Condition

在本節中,我們測試使用 @ConditionalOnPropertyannotations的自動配置類。

首先,我們需要這個測試的屬性:

com.baeldung.service=custom

然後,我們編寫嵌套的自動配置類,根據前面的屬性創建bean:

@Configuration
@TestPropertySource("classpath:ConditionalOnPropertyTest.properties") protected static class SimpleServiceConfiguration {     @Bean     @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "default")     @ConditionalOnMissingBean     public DefaultService defaultService() {         return new DefaultService();     }     @Bean @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "custom") @ConditionalOnMissingBean public CustomService customService() { return new CustomService(); } } 

現在,我們調用withPropertyValues方法來覆蓋每個測試中的屬性值:

@Test
public void whenGivenCustomPropertyValue_thenCustomServiceCreated() { this.contextRunner.withPropertyValues("com.baeldung.service=custom") .withUserConfiguration(SimpleServiceConfiguration.class) .run(context -> { assertThat(context).hasBean("customService"); SimpleService simpleService = context.getBean(CustomService.class); assertThat(simpleService.serve()).isEqualTo("Custom Service"); assertThat(context).doesNotHaveBean("defaultService"); }); } @Test public void whenGivenDefaultPropertyValue_thenDefaultServiceCreated() { this.contextRunner.withPropertyValues("com.baeldung.service=default") .withUserConfiguration(SimpleServiceConfiguration.class) .run(context -> { assertThat(context).hasBean("defaultService"); SimpleService simpleService = context.getBean(DefaultService.class); assertThat(simpleService.serve()).isEqualTo("Default Service"); assertThat(context).doesNotHaveBean("customService"); }); } 

3. 結論

總結一下, 這篇教程主要展示 如何使用ApplicationContextRunner運行帶有自定義的ApplicationContext並應用斷言.

我們在這裏介紹了最常用的場景,而不是列出如何自定義ApplicationContext 。

在此期間,請記住ApplicationConetxtRunner適用於非Web應用程序,因此請考慮WebApplicationContextRunner用於基於servlet的Web應用程序,ReactiveWebApplicationContextRunner用於響應式Web應用程序。

本文源代碼,請訪問GitHub。

原文:www.baeldung.com/spring-boot…

作者:baeldung

譯者:Leesen

 

【精選推薦文章】

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

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

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

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

為什麼阿里巴巴要求謹慎使用ArrayList中的subList方法

GitHub 3.7k Star 的Java工程師成神之路 ,不來了解一下嗎?

GitHub 3.7k Star 的Java工程師成神之路 ,真的不來了解一下嗎?

GitHub 3.7k Star 的Java工程師成神之路 ,真的確定不來了解一下嗎?

集合是Java開發日常開發中經常會使用到的。在之前的一些文章中,我們介紹過一些關於使用集合類應該注意的事項,如《為什麼阿里巴巴禁止在 foreach 循環里進行元素的 remove/add 操作》、《為什麼阿里巴巴建議集合初始化時,指定集合容量大小》等。

關於集合類,《阿里巴巴Java開發手冊》中其實還有另外一個規定:

本文就來分析一下為什麼會有如此建議?其背後的原理是什麼?

subList

subList是List接口中定義的一個方法,該方法主要用於返回一個集合中的一段、可以理解為截取一個集合中的部分元素,他的返回值也是一個List。

如以下代碼:

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    List subList = names.subList(0, 1);
    System.out.println(subList);
}

以上代碼輸出結果為:

[Hollis]

如果我們改動下代碼,將subList的返回值強轉成ArrayList試一下:

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    ArrayList subList = names.subList(0, 1);
    System.out.println(subList);
}

以上代碼將拋出異常:

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

不只是強轉成ArrayList會報錯,強轉成LinkedList、Vector等List的實現類同樣也都會報錯。

那麼,為什麼會發生這樣的報錯呢?我們接下來深入分析一下。

底層原理

首先,我們看下subList方法給我們返回的List到底是個什麼東西,這一點在JDK源碼中註釋是這樣說的:

Returns a view of the portion of this list between the specifiedfromIndex, inclusive, and toIndex, exclusive.

也就是說subList 返回是一個視圖,那麼什麼叫做視圖呢?

我們看下subList的源碼:

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

這個方法返回了一個SubList,這個類是ArrayList中的一個內部類。

SubList這個類中單獨定義了set、get、size、add、remove等方法。

當我們調用subList方法的時候,會通過調用SubList的構造函數創建一個SubList,那麼看下這個構造函數做了哪些事情:

SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;
}

可以看到,這個構造函數中把原來的List以及該List中的部分屬性直接賦值給自己的一些屬性了。

也就是說,SubList並沒有重新創建一個List,而是直接引用了原有的List(返回了父類的視圖),只是指定了一下他要使用的元素的範圍而已(從fromIndex(包含),到toIndex(不包含))。

所以,為什麼不能講subList方法得到的集合直接轉換成ArrayList呢?因為SubList只是ArrayList的內部類,他們之間並沒有集成關係,故無法直接進行強制類型轉換。

視圖有什麼問題

前面通過查看源碼,我們知道,subList()方法並沒有重新創建一個ArrayList,而是返回了一個ArrayList的內部類——SubList。

這個SubList是ArrayList的一個視圖。

那麼,這個視圖又會帶來什麼問題呢?我們需要簡單寫幾段代碼看一下。

1、非結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    subList.set(1, "666");

    System.out.println("subList.set(3,666) 得到List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}

得到結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 得到List :
subList : [L, L, I]
subList.set(3,666) 得到List :
subList : [L, 666, I]
sourceList : [H, O, L, 666, I, S]

當我們嘗試通過set方法,改變subList中某個元素的值得時候,我們發現,原來的那個List中對應元素的值也發生了改變。

同理,如果我們使用同樣的方法,對sourceList中的某個元素進行修改,那麼subList中對應的值也會發生改變。讀者可以自行嘗試一下。

1、結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    subList.add("666");

    System.out.println("subList.add(666) 得到List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}

得到結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 得到List :
subList : [L, L, I]
subList.add(666) 得到List :
subList : [L, L, I, 666]
sourceList : [H, O, L, L, I, 666, S]

我們嘗試對subList的結構進行改變,即向其追加元素,那麼得到的結果是sourceList的結構也同樣發生了改變。

1、結構性改變原List

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 得到List :");
    System.out.println("subList : " + subList);

    sourceList.add("666");

    System.out.println("sourceList.add(666) 得到List :");
    System.out.println("sourceList : " + sourceList);
    System.out.println("subList : " + subList);

}

得到結果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
    at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
    at java.util.AbstractList.listIterator(AbstractList.java:299)
    at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
    at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    at java.lang.String.valueOf(String.java:2994)
    at java.lang.StringBuilder.append(StringBuilder.java:131)
    at com.hollis.SubListTest.main(SubListTest.java:28)

我們嘗試對sourceList的結構進行改變,即向其追加元素,結果發現拋出了ConcurrentModificationException。關於這個異常,我們在《一不小心就踩坑的fail-fast是個什麼鬼?》中分析過,這裏原理相同,就不再贅述了。

小結

我們簡單總結一下,List的subList方法並沒有創建一個新的List,而是使用了原List的視圖,這個視圖使用內部類SubList表示。

所以,我們不能把subList方法返回的List強制轉換成ArrayList等類,因為他們之間沒有繼承關係。

另外,視圖和原List的修改還需要注意幾點,尤其是他們之間的相互影響:

1、對父(sourceList)子(subList)List做的非結構性修改(non-structural changes),都會影響到彼此。

2、對子List做結構性修改,操作同樣會反映到父List上。

3、對父List做結構性修改,會拋出異常ConcurrentModificationException。

所以,阿里巴巴Java開發手冊中有另外一條規定:

如何創建新的List

如果需要對subList作出修改,又不想動原list。那麼可以創建subList的一個拷貝:

subList = Lists.newArrayList(subList);
list.stream().skip(strart).limit(end).collect(Collectors.toList());

PS:最近,《阿里巴巴Java開發手冊》已經正式更名為《Java開發手冊》,併發布了新版本,增加了21條新規約,修改描述112處。

關注公眾號後台回復:手冊,即可獲取最新版Java開發手冊。

參考資料: https://www.jianshu.com/p/5854851240df https://www.cnblogs.com/ljdblog/p/6251387.html

【精選推薦文章】

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

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

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

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