[WPF自定義控件庫]使用TextBlockHighlightSource強化高亮的功能,以及使用TypeConverter簡化調用

1. 強化高亮的功能

上一篇文章介紹了使用附加屬性實現TextBlock的高亮功能,但也留下了問題:不能定義高亮(或者低亮)的顏色。為了解決這個問題,我創建了TextBlockHighlightSource這個類,比單純的字符串存儲更多的信息,這個類的定義如下:

相應地,附加屬性的類型也改變為這個類,並且屬性值改變事件改成這樣:

private static void OnHighlightTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var oldValue = (TextBlockHighlightSource)args.OldValue;
    var newValue = (TextBlockHighlightSource)args.NewValue;
    if (oldValue == newValue)
        return;

    void OnPropertyChanged(object sender,EventArgs e)
    {
        if (obj is TextBlock target)
        {
            MarkHighlight(target, newValue);
        }
    };

    if(oldValue!=null)
        newValue.PropertyChanged -= OnPropertyChanged;

    if (newValue != null)
        newValue.PropertyChanged += OnPropertyChanged;

    OnPropertyChanged(null, null);
}

MarkHighlight的關鍵代碼修改為這樣:

if (highlightSource.LowlightForeground != null)
    run.Foreground = highlightSource.LowlightForeground;

if (highlightSource.HighlightForeground != null)
    run.Foreground = highlightSource.HighlightForeground;

if (highlightSource.HighlightBackground != null)
    run.Background = highlightSource.HighlightBackground;

使用起來就是這樣:

<TextBlock Text="Git hub"
           TextWrapping="Wrap">
    <kino:TextBlockService.HighlightText>
        <kino:TextBlockHighlightSource Text="hub"
                                       LowlightForeground="Black"
                                       HighlightBackground="#FFF37D33" />
    </kino:TextBlockService.HighlightText>
</TextBlock>

2. 使用TypeConverter簡化調用

TextBlockHighlightSource提供了很多功能,但和直接使用字符串比起來,創建一個TextBlockHighlightSource要複雜多。為了可以簡化調用可以使用自定義的TypeConverter

首先來了解一下TypeConverter的概念。XAML本質上是XML,其中的屬性內容全部都是字符串。如果對應屬性的類型是XAML內置類型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等類型),XAML解析器直接將字符串轉換成對應值賦給屬性;對於其它類型,XAML解析器需做更多工作。

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

如上面這段XAML中的”Auto”和”*”,XAML解析器將其分別解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再賦值給Height,它相當於這段代碼:

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

為了完成這個工作,XAML解析器需要TypeConverter的協助。XAML解析器通過兩個步驟查找TypeConverter:
1. 檢查屬性聲明上的TypeConverterAttribute。
2. 如果屬性聲明中沒有TypeConverterAttribute,檢查類型聲明中的TypeConverterAttribute。

屬性聲明上TypeConverterAttribute的優先級高於類型聲明。如果以上兩步都找不到類型對應的TypeConverterAttribute,XAML解析器將會報錯:屬性”*”的值無效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器調用它的object ConvertFromString(string text)函數將字符串轉換成屬性的值。

WPF內置的TypeConverter十分十分多,但有時還是需要自定義TypeConverter,自定義TypeConverter的基本步驟如下:

  • 創建一個繼承自TypeConverter的類;
  • 重寫virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重寫virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重寫virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重寫virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

到這裏我想TypeConverter的概念已經介紹得夠詳細了。回到本來話題,要簡化TextBlockHighlightSource的調用我創建了TextBlockHighlightSourceConverter這個類,它繼承自TypeConverter,裏面的關鍵代碼如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    switch (value)
    {
        case null:
            throw GetConvertFromException(null);
        case string source:
            return new TextBlockHighlightSource { Text = value.ToString() };
    }

    return base.ConvertFrom(context, culture, value);
}

然後在TextBlockHighlightSource上使用TypeConverterAttribute:

[TypeConverter(typeof(TextBlockHighlightSourceConverter))]
public class TextBlockHighlightSource : FrameworkElement

這樣在XAML中TextBlockHighlightSource的調用方式就可以和使用字符串一樣簡單了。

<TextBlock Text="Github"
           kino:TextBlockService.HighlightText="hub" />

3. 使用Style

有沒有發現TextBlockHighlightSource繼承自FrameworkElement?這種奇特的寫法是為了讓TextBlockHighlightSource可以使用全局的Style。畢竟要在應用程序里統一Highlight的顏色還是全局樣式最好使,但作為附加屬性,TextBlockHighlightSource並不是VisualTree的一部分,它拿不到VisualTree上的Resources。最簡單的解決方案是讓TextBlockHighlightSource繼承自FrameworkElement,把它放到VisualTree里,用法如下:

<StackPanel>
    <FrameworkElement.Resources>
        <Style TargetType="kino:TextBlockHighlightSource">
            <Setter Property="LowlightForeground" Value="Blue"/>
        </Style>
    </FrameworkElement.Resources>
    <TextBox x:Name="FilterElement3"/>
    <kino:TextBlockHighlightSource Text="{Binding ElementName=FilterElement3,Path=Text}" 
                                   HighlightForeground="DarkBlue"
                                   HighlightBackground="Yellow"
                                   x:Name="TextBlockHighlightSource2"/>
    <TextBlock Text="A very powerful projector with special features for Internet usability, USB" 
               kino:TextBlockService.HighlightText="{Binding ElementName=TextBlockHighlightSource2}"
               TextWrapping="Wrap"/>
</StackPanel>

也許你會覺得這種寫法有些奇怪,畢竟我也覺得在View上放一個隱藏的元素真的很怪。其實在一萬二千年前微軟就已經有這種寫法,在DomainDataSource的文檔里就有用到:

<Grid x:Name="LayoutRoot" Background="White">  
    <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <riaControls:DomainDataSource x:Name="source" QueryName="GetProducts" AutoLoad="true">
        <riaControls:DomainDataSource.DomainContext>
            <domain:ProductDomainContext />
        </riaControls:DomainDataSource.DomainContext>   
        <riaControls:DomainDataSource.FilterDescriptors>
            <riaData:FilterDescriptorCollection LogicalOperator="And">
              <riaData:FilterDescriptor PropertyPath="Color" Operator="IsEqualTo" Value="Blue" />
              <riaData:FilterDescriptor PropertyPath="ListPrice" Operator="IsLessThanOrEqualTo">
                  <riaControls:ControlParameter 
                      ControlName="MaxPrice" 
                      PropertyName="SelectedItem.Content" 
                      RefreshEventName="SelectionChanged" />
              </riaData:FilterDescriptor>
            </riaData:FilterDescriptorCollection>
        </riaControls:DomainDataSource.FilterDescriptors>
    </riaControls:DomainDataSource>
    <ComboBox x:Name="MaxPrice" Grid.Row="0" Width="60" SelectedIndex="0">
        <ComboBoxItem Content="100" />
        <ComboBoxItem Content="500" />
        <ComboBoxItem Content="1000" />
    </ComboBox>
    <data:DataGrid Grid.Row="1" ItemsSource="{Binding Data, ElementName=source}" />
</Grid>

把DataSource放到View上這種做法可能是WinForm的祖傳家訓,結構可恥但有用。

4. 結語

寫這篇博客的時候我才發覺這個附加屬性還叫HighlightText好像不太好,但也懶得改了。

這篇文章介紹了使用TypeConverter簡化調用,以及繼承自FrameworkElement以便使用Style。

5. 參考

TypeConverter 類
TypeConverters 和 XAML
Type Converters for XAML Overview
TypeConverterAttribute Class
如何:實現類型轉換器

6. 源碼

TextBlock at master · DinoChan_Kino.Toolkit.Wpf

【精選推薦文章】

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

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

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

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

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

一、前言

  寫這篇博文的主要目的如下:

  • 通過相關類和接口分析IoC容器到底長什麼樣。
  • 闡述筆者對Spring上下文和容器的理解。
  • 介紹重要的類輔助理解SpringBoot的啟動流程。

 

二、Spring IoC容器的設計

  看看下面這張圖(摘自《Spring技術內幕》),IoC容器的設計分為兩條線,

  1.   BeanFactory ==> HierarchicalBeanFactory ==>ConfigurableBeanFactory ,這條線可以理解成IoC容器的設計路線。
  2.   BeanFactory ==> ListableBeanFactory ==> ApplicationContext ==> ConfigurableApplicationContext ,這條可以成為Spring應用上下文的設計路線。

  為什麼這樣要分兩條線呢,主要是將容器和上下文區分開來。因為在在Spring項目中,上下文對容器不僅是擴展的關係,更重要的是持有的關係,上下文是以屬性的形式持有了容器,開發者可以通過上下文對象獲取到容器。筆者十分傾向於將二者分開來理解。當然也可以將應用上下文理解成容器的高級表現形式。

 

2.1,IoC容器的設計線路

  BeanFactory定義了IoC容器的基本規範,包括getBean()按類型和按名稱的獲取Bean的方法。

   

  HierarchicalBeanFactory 在BeanFactory的基礎上增加了getParentBeanFactory()方法,使BeanFactory具備了雙親IoC容器管理的功能。

  ConfigurableBeanFactory接口提供了配置BeanFactory的各種方法。比如setParentBeanFactory()方法,配置上面提到的雙親IoC容器,addBeanPostProcessor()方法,配置Bean後置處理器等。

  到這裏先埋個包袱:到ConfigurableBeanFactory接口為止,IoC容器還沒有具備作為“容器”最基本的功能,那就是能裝東西。

 

2.2、應用上下文設計路線

  上面說了應用上下文是IoC容器的高級表現形式,ListableBeanFactory具備了操作BeanDefinition 的能力,比如getBeanDefinitionCount()方法,可以獲取Bean的總數等。

  ApplicationContext 類那就厲害了,如下代碼所示,實現了一大堆接口

1 public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
2         MessageSource, ApplicationEventPublisher, ResourcePatternResolver
  • MessageSource,支持不同的信息源。具備支持國際化的實現,為開發多語言版本的應用提供服務。
  • ResourcePatternResolver,訪問數據源。具備了從不同地方得到Bean定義資源的能力,比如:xml,java config,註解等等。
  • ApplicationEventPublisher,發布事件。使應用上下文具備了事件機制。事件機製為Bean聲明周期的管理提供了便利。

  WebApplicationContext擴展了對web應用的支持。

  ConfigurableApplicationContext就更重要了,看過Spring源碼的都知道一個重要的方法叫refresh,沒錯就是在這個接口中定義的。最重要的是擴展了配置上下文的功能,和控制上下文生命周期的能力等等。

 

三、IoC容器的具體實現類 DefaultListableBeanFactory(重點)

  首先證明一點,為什麼說DefaultListableBeanFactory類是具體的實現類呢?

  隨便啟動一個SpringBoot項目找到第25行代碼(在SpringBoot的啟動流程系列博文中有介紹)

 1 public ConfigurableApplicationContext run(String... args) {
 2     //記錄程序運行時間
 3     StopWatch stopWatch = new StopWatch();
 4     stopWatch.start();
 5     // ConfigurableApplicationContext Spring 的上下文
 6     ConfigurableApplicationContext context = null;
 7     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
 8     configureHeadlessProperty();
 9     //從META-INF/spring.factories中獲取監聽器
10     //1、獲取並啟動監聽器
11     SpringApplicationRunListeners listeners = getRunListeners(args);
12     listeners.starting();
13     try {
14         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
15                 args);
16         //2、構造容器環境
17         ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
18         //處理需要忽略的Bean
19         configureIgnoreBeanInfo(environment);
20         //打印banner
21         Banner printedBanner = printBanner(environment);
22         ///3、初始化容器
23         context = createApplicationContext();
24         //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
25         exceptionReporters = getSpringFactoriesInstances(
26                 SpringBootExceptionReporter.class,
27                 new Class[]{ConfigurableApplicationContext.class}, context);
28         //4、刷新容器前的準備階段
29         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
30         //5、刷新容器
31         refreshContext(context);
32         //刷新容器后的擴展接口
33         afterRefresh(context, applicationArguments);
34         stopWatch.stop();
35         if (this.logStartupInfo) {
36             new StartupInfoLogger(this.mainApplicationClass)
37                     .logStarted(getApplicationLog(), stopWatch);
38         }
39         listeners.started(context);
40         callRunners(context, applicationArguments);
41     } catch (Throwable ex) {
42         handleRunFailure(context, ex, exceptionReporters, listeners);
43         throw new IllegalStateException(ex);
44     }
45 
46     try {
47         listeners.running(context);
48     } catch (Throwable ex) {
49         handleRunFailure(context, ex, exceptionReporters, null);
50         throw new IllegalStateException(ex);
51     }
52     return context;
53 }

  debug

  如2標註點所示IoC容器的真實面孔就是這個DefaultListableBeanFactory類了。當然他還有一個子類XmlBeanFactory,不過都已經被標註為棄用了(@Deprecated)在《Spring技術內幕》這本書裏面也是着重的講的這個類,可能當時作者是以SpringMVC項目來講解的吧。XmlBeanFactory顧名思義就是提供了對xml配置方式的支持。呃。。。又勾起了用SpringMVC的痛苦回憶。

  言歸正傳,

  如下圖,看看他的繼承關係

   章節二中提到了很多的IoC容器系列,這樣總結一下吧,俗話說一流企業做標準,二流企業做產品,章節二中的那一坨就是IoC容器的實現標準,本章節我們要總結的類DefaultListableBeanFactory就是IoC容器的具體產品。

  看見上圖中那一堆接口和類是不是有點懵,沒關係,咱們慢慢梳理一下。

 

3.1,作為IoC容器的基礎設計路線

  這條線路在上一章節中已經梳理過了。只是多出了ConfigurableListableBeanFactory接口,ConfigurableListableBeanFactory接口主要是增加指定忽略類型和接口等

 

3.2、作為IoC容器的高級設計路線

  這條設計路線乍一看還是挺複雜的,的確是這樣。

  1, BeanFactory ==> AutowireCapableBeanFactory ==> AbstractAutowireCapableBeanFactory ==> DefaultListableBeanFactory 

  在這條線路中,AutowireCapableBeanFactory接口定義了自動注入bean(autowireBean()),創建bean(createBean()),初始化bean(initializeBean())方法等。那麼真正實現這些方法的類便是AbstractAutowireCapableBeanFactory。

  AbstractAutowireCapableBeanFactory抽象類中實現了AutowireCapableBeanFactory接口定義的方法。在此基礎上通過繼承AbstractBeanFactory具備了操作Bean的能力。

  2, SingletonBeanRegistry ==> DefaultSingletonBeanRegistry ==> FactoryBeanRegistrySupport ==> AbstractBeanFactory ==> AutowireCapableBeanFactory ==> DefaultListableBeanFactory 

  這條關係鏈有點長,在這條鏈中我們要關心的是SingletonBeanRegistry接口,顧名思義,這個接口是單例Bean的註冊接口。當然也不止註冊這麼簡單。如下圖中所示,除了註冊單例之外,還定義獲取單例的方法。

  注意:為什麼只有singleton的註冊中心,而沒有prototype類型的Bean的註冊中心呢?因為單例Bean(singleton)是Spring幫我們創建的並維護的,原型Bean(prototype)是每次獲取都會創建出來一個實例。本質是不同的。

  3, AliasRegistry ==> SimpleAliasRegistry ==> DefaultSingletonBeanRegistry ==> FactoryBeanRegistrySupport ==> AbstractBeanFactory ==> AutowireCapableBeanFactory ==> DefaultListableBeanFactory 

   這條路線呢,主要是提供管理別稱的能力。因為不是重點,在此就不詳細分析了。

  4, AliasRegistry ==> BeanDefinitionRegistry ==> DefaultListableBeanFactory 

  BeanDefinitionRegistry接口要重點說一下,該接口是BeanDefinition的註冊中心。使DefaultListableBeanFactory具備操作BeanDefinition的能力。看一下它有什麼方法。

  包括了註冊,刪除,獲取BeanDefinition的方法。當然這隻是個接口,這些方法的具體實現在DefaultListableBeanFactory中。

 

3.3、DefaultListableBeanFactory幾個重要的父類和接口

3.3.1, AbstractBeanFactory 抽象類

  如上圖所示,AbstractBeanFactory中實現了BeanFactory中定義的幾個重要的方法。常用的註解 @Autowired @Resource(name = “xxx”) 大家都知道一個是按類查找,一個是按名獲取。具體實現這兩個註解的方法就是上圖中圈出來的幾個方法。幾個getBean()方法最終都進入了doGetBean()方法。doGetBean()方法是實際獲得Bean的地方,也是觸發依賴注入發生的地方。在SpringBoot啟動流程總會對這個方法進行詳細的介紹。

3.3.2, AbstractAutowireCapableBeanFactory 抽象類

  AbstractBeanFactory中實現了getBean()方法,AbstractAutowireCapableBeanFactory中實現了Bean的創建方法。

  當我們需要定義一個Bean通常會有這樣寫 @Bean(name = “test”, initMethod = “init”, destroyMethod = “destroy”) 。AbstractAutowireCapableBeanFactory中完成了一個Bean從 create(createBean()) ==> createInstance(createBeanInstance()) ==> init(invokeInitMethods()) 的所有工作。所以這個抽象類的作用不言而喻。具體的創建過程,會在SpringBoot的啟動流程中詳細介紹。

3.3.3, DefaultSingletonBeanRegistry 讓IoC容器擁有作為“容器”的能力

  其實我們經常說的Spring 容器,這個容器其實更多的是BeanFactory所代表的意義:Bean生產工廠。是滴,通常我們理解的容器應該是能裝東西的,但是spring 容器不是代表代表水桶一樣的東西,而是像富士康一樣,生產東西的地方。比如我們需要一個Bean,我們只需要告訴spring,spring就會給我們。所以到目前為止我們還沒有看到IoC作為“容器”的能力。以上言論純屬自己理解,不喜勿噴。

  DefaultSingletonBeanRegistry屬性先貼出來

 1 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
 2     /**
 3      * Cache of singleton objects: bean name --> bean instance
 4      * 緩存 單例對象
 5      */
 6     private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);//好習慣,創建ConcurrentHashMap,指定初始化因子,縱觀Spring源碼,創建HashMap,都有初始化因子。get
 7 
 8     /**
 9      * Cache of singleton factories: bean name --> ObjectFactory
10      * 緩存 單例工廠
11      */
12     private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
13 
14     /**
15      * Cache of early singleton objects: bean name --> bean instance
16      * 緩存 提前暴露的對象
17      */
18     private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
19 
20     /**
21      * Set of registered singletons, containing the bean names in registration order
22      * 已經註冊的單例對象集合,按照註冊順序排序的,並且是不可重複的。
23      */
24     private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
25 
26     /**
27      * Names of beans that are currently in creation
28      *
29      */
30     private final Set<String> singletonsCurrentlyInCreation =
31             Collections.newSetFromMap(new ConcurrentHashMap<>(16));
32 
33     /**
34      * Names of beans currently excluded from in creation checks
35      */
36     private final Set<String> inCreationCheckExclusions =
37             Collections.newSetFromMap(new ConcurrentHashMap<>(16));
38 
39     /**
40      * List of suppressed Exceptions, available for associating related causes
41      */
42     @Nullable
43     private Set<Exception> suppressedExceptions;
44 
45     /**
46      * Flag that indicates whether we're currently within destroySingletons
47      */
48     private boolean singletonsCurrentlyInDestruction = false;
49 
50     /**
51      * Disposable bean instances: bean name --> disposable instance
52      * spring是作為一個註冊中心的樣子,在容器shutdown的時候,直接從這裏面找到需要執行destory鈎子的Bean
53      */
54     private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
55 
56     /**
57      * Map between containing bean names: bean name --> Set of bean names that the bean contains
58      * 名稱為name的bean,所持有的beans 的映射關係
59      */
60     private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
61 
62     /**
63      * Map between dependent bean names: bean name --> Set of dependent bean names
64      * 名稱為name的bean與其所依賴的bean的映射關係
65      */
66     private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
67 
68     /**
69      * Map between depending bean names: bean name --> Set of bean names for the bean's dependencies
70      */
71     private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);
72 }

 

  屬性singletonObjects ,沒錯,就是這個東東,最終存儲單例(singleton)Bean的地方,在SpringBoot啟動流程中,會詳細介紹存取的過程。上面說了原型(prototype)Bean是不需要緩存的,不解釋了。到這裏我們初步看到了IoC作為“容器”該有的樣子。

  DefaultSingletonBeanRegistry上面提到的SingletonBeanRegistry接口的相關方法,並且增加了很多對單例的操作的方法。

3.3.4, DefaultListableBeanFactory (重點)

  上面我們從IoC容器的宏觀設計角度闡述了DefaultListableBeanFactory作為IoC容器的具體實現的設計思想。在這裏來分析一下這個類本身的設計。

  首先看看該類的屬性

 1 public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
 2         implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
 3     /**
 4      * Map from serialized id to factory instance
 5      * 緩存 序列化ID到 DefaultListableBeanFactory 實例的映射
 6      */
 7     private static final Map<String, Reference<DefaultListableBeanFactory>> serializableFactories =
 8             new ConcurrentHashMap<>(8);
 9 
10     /**
11      * Optional id for this factory, for serialization purposes
12      */
13     @Nullable
14     private String serializationId;
15 
16     /**
17      * Whether to allow re-registration of a different definition with the same name
18      */
19     private boolean allowBeanDefinitionOverriding = true;
20 
21     /**
22      * Whether to allow eager class loading even for lazy-init beans
23      */
24     private boolean allowEagerClassLoading = true;
25 
26     /**
27      * Optional OrderComparator for dependency Lists and arrays
28      */
29     @Nullable
30     private Comparator<Object> dependencyComparator;
31 
32     /**
33      * Resolver to use for checking if a bean definition is an autowire candidate
34      * 被用來解決去校驗一個BeanDefinition是不是自動裝載的候選人
35      */
36     private AutowireCandidateResolver autowireCandidateResolver = new SimpleAutowireCandidateResolver();
37 
38     /**
39      * Map from dependency type to corresponding autowired value
40      * 緩存 類型對應的自動裝載的Bean
41      */
42     private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
43 
44     /**
45      * Map of bean definition objects, keyed by bean name
46      * 緩存 beanName到BeanDefinition的映射關係
47      */
48     private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
49 
50     /**
51      * Map of singleton and non-singleton bean names, keyed by dependency type
52      * 緩存 類型 和 beanName的映射關係
53      */
54     private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
55 
56     /**
57      * Map of singleton-only bean names, keyed by dependency type
58      * 緩存 類型 和 單例Bean names的映射
59      */
60     private final Map<Class<?>, String[]> singletonBeanNamesByType = new ConcurrentHashMap<>(64);
61 
62     /**
63      * List of bean definition names, in registration order
64      * 緩存 beanDefinition name的list
65      */
66     private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
67 
68     /**
69      * List of names of manually registered singletons, in registration order
70      */
71     private volatile Set<String> manualSingletonNames = new LinkedHashSet<>(16);
72 
73     /**
74      * Cached array of bean definition names in case of frozen configuration
75      */
76     @Nullable
77     private volatile String[] frozenBeanDefinitionNames;
78 
79     /**
80      * Whether bean definition metadata may be cached for all beans
81      */
82     private volatile boolean configurationFrozen = false;
83 }

  在Spring中,實際上是把DefaultListableBeanFactory作為一個默認的功能完整的IoC容器來使用。 DefaultListableBeanFactory作為一個功能完整的容器具備了除以上父類所具有功能外,還加入了對BeanDefinition的管理和維護。從上面的代碼可以看到一個重要的屬性:beanDefinitionMap。beanDefinitionMap緩存了Bean name到 BeanDefinition的映射。到這裡是不是發現了IoC容器另外一個作為“容器”的能力。在我的理解範圍內,IoC容器作為“容器”真正裝的兩個最總要的能力算是總結完了,一個是裝單例(Singleton)Bean,一個是裝BeanDefinition。

 

3.3.5, BeanDefinition 

  Spring通過定義BeanDefinition來管理基於Spring的應用中的各種對象以及他們之間的相互依賴關係。BeanDefinition抽象了我們對Bean的定義,是讓容器起作用的主要數據類型。我么都知道在計算機世界里,所有的功能都是建立在通過數據對現實進行抽象的基礎上的。IoC容器是用來管理對象依賴關係的,對IoC容器來說,BeanDefinition就是對依賴反轉模式中管理的對象依賴關係的數據抽象,也是容器實現依賴反轉功能的核心數據結構,依賴反轉功能都是圍繞對這個BeanDefinition的處理來完成的。這些BeanDefinition就像是容器里裝的水,有了這些基本數據,容器才能發揮作用。簡單一句話來說,BeanDefinition就是Bean的元數據,BeanDefinition存放了對Bean的基本描述,包括Bean擁有什麼屬性,方法,Bean的位置等等Bean的各種信息。IoC容器可以通過BeanDefinition生成Bean。

  BeanDefinition究竟長什麼樣呢?

  在同第三章debug的地方一樣,點開beanFactory,然後查看beanDefinitionMap屬性。

  OK,BeanDefinition就是長這樣了。具體怎麼通過它生成Bean,在SpringBoot啟動流程中會詳細介紹。

 

四、SpringBoot web工程中的上下文 AnnotationConfigServletWebServerApplicationContext

  在SpringBoot工程中,應用類型分為三種,如下代碼所示。

 1 public enum WebApplicationType {
 2     /**
 3      * 應用程序不是web應用,也不應該用web服務器去啟動
 4      */
 5     NONE,
 6     /**
 7      * 應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web(tomcat)服務器。
 8      */
 9     SERVLET,
10     /**
11      * 應用程序應作為 reactive web應用程序運行,並應啟動嵌入式 reactive web服務器。
12      */
13     REACTIVE
14 }

  對應三種應用類型,SpringBoot項目有三種對應的應用上下文,我們以web工程為例,即其上下文為AnnotationConfigServletWebServerApplicationContext

 1 public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
 2         + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
 3 public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
 4         + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
 5 public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
 6         + "annotation.AnnotationConfigApplicationContext";
 7         
 8 protected ConfigurableApplicationContext createApplicationContext() {
 9     Class<?> contextClass = this.applicationContextClass;
10     if (contextClass == null) {
11         try {
12             switch (this.webApplicationType) {
13                 case SERVLET:
14                     contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
15                     break;
16                 case REACTIVE:
17                     contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
18                     break;
19                 default:
20                     contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
21             }
22         } catch (ClassNotFoundException ex) {
23             throw new IllegalStateException(
24                     "Unable create a default ApplicationContext, "
25                             + "please specify an ApplicationContextClass",
26                     ex);
27         }
28     }
29     return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
30 }

  我們先看一下AnnotationConfigServletWebServerApplicationContext的設計。 

   在2.2中已經介紹了ApplicationContext的設計

  關於AnnotationConfigServletWebServerApplicationContext詳細的設計路線在這裏就不像DefaultListableBeanFactory容器那麼詳細的去講解了。在第二章說過,應用上下文可以理解成IoC容器的高級表現形式,拿上圖和DefaultListableBeanFactory的繼承關係圖,不難發現,應用上下文確實是在IoC容器的基礎上豐富了一些高級功能。在第二章中,我們還說過應用上下文對IoC容器是持有的關係。繼續看第二章debug的截圖,context就是AnnotationConfigServletWebServerApplicationContext的神秘面孔,他的一個屬性beanFactory就是IoC容器(DefaultListableBeanFactory)。所以他們之間是持有,和擴展的關係。

  接下來看GenericApplicationContext類

1 public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
2     private final DefaultListableBeanFactory beanFactory;
3     ...
4 }

   第一行赫然定義了beanFactory屬性,正是DefaultListableBeanFactory對象。

  關於上下文還有另外一個最重要的方法refresh,上文中說道該方法是在ConfigurableApplicationContext接口中定義的,那麼在哪實現的該方法呢?

  看AbstractApplicationContext類。

 1 @Override
 2 public void refresh() throws BeansException, IllegalStateException {
 3     synchronized (this.startupShutdownMonitor) {
 4         // Prepare this context for refreshing.
 5         //刷新上下文環境
 6         prepareRefresh();
 7 
 8         // Tell the subclass to refresh the internal bean factory.
 9         //這裡是在子類中啟動 refreshBeanFactory() 的地方
10         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
11 
12         // Prepare the bean factory for use in this context.
13         //準備bean工廠,以便在此上下文中使用
14         prepareBeanFactory(beanFactory);
15 
16         try {
17             // Allows post-processing of the bean factory in context subclasses.
18             //設置 beanFactory 的後置處理
19             postProcessBeanFactory(beanFactory);
20 
21             // Invoke factory processors registered as beans in the context.
22             //調用 BeanFactory 的后處理器,這些處理器是在Bean 定義中向容器註冊的
23             invokeBeanFactoryPostProcessors(beanFactory);
24 
25             // Register bean processors that intercept bean creation.
26             //註冊Bean的后處理器,在Bean創建過程中調用
27             registerBeanPostProcessors(beanFactory);
28 
29             // Initialize message source for this context.
30             //對上下文中的消息源進行初始化
31             initMessageSource();
32 
33             // Initialize event multicaster for this context.
34             //初始化上下文中的事件機制
35             initApplicationEventMulticaster();
36 
37             // Initialize other special beans in specific context subclasses.
38             //初始化其他特殊的Bean
39             onRefresh();
40 
41             // Check for listener beans and register them.
42             //檢查監聽Bean並且將這些監聽Bean向容器註冊
43             registerListeners();
44 
45             // Instantiate all remaining (non-lazy-init) singletons.
46             //實例化所有的(non-lazy-init)單件
47             finishBeanFactoryInitialization(beanFactory);
48 
49             // Last step: publish corresponding event.
50             //發布容器事件,結束Refresh過程
51             finishRefresh();
52         } catch (BeansException ex) {
53             if (logger.isWarnEnabled()) {
54                 logger.warn("Exception encountered during context initialization - " +
55                         "cancelling refresh attempt: " + ex);
56             }
57 
58             // Destroy already created singletons to avoid dangling resources.
59             destroyBeans();
60 
61             // Reset 'active' flag.
62             cancelRefresh(ex);
63 
64             // Propagate exception to caller.
65             throw ex;
66         } finally {
67             // Reset common introspection caches in Spring's core, since we
68             // might not ever need metadata for singleton beans anymore...
69             resetCommonCaches();
70         }
71     }
72 }

   OK,應用上下文就介紹到這裏。

 

 五、IoC容器的初始化過程

   在這裏我們先口述一下IoC容器的初始化過程吧,源碼分析,請移步SpringBoot啟動流程分析。

  簡單來說IoC容器的初始化過程是由前面介紹的refresh()方法啟動的,這個方法標志著IoC容器的正式啟動。具體來說,這個啟動包括三個過程

1 BeanDefinition的Resource定位
2 BeanDefinition的載入
3 向IoC容器註冊BeanDefinition

 

   1、第一個過程:Resource定位

  這個定位指的是BeanDefinition的資源定位,它由ResourceLoader通過統一的Resource接口完成,這個Resource對各種形式的BeanDefinition的使用都提供了統一接口。對於這些BeanDefinition的存在形式,可以是通過像SpringMVC中的xml定義的Bean,也可以是像在類路徑中的Bean定義信息,比如使用@Component等註解定義的。這個過程類似於容器尋找數據的過程,就像用水桶裝水先要把水找到一樣。

  結合SpringBoot說一下這個過程,對於SpringBoot,我們都知道他的包掃描是從主類所在的包開始掃描的,那這個定位的過程在SpringBoot中具體是這樣的,在refresh容器之前(prepareContext()方法中),會先將主類解析成BeanDefinition,然後在refresh方法中並且是掃描Bean之前,解析主類的BeanDefinition獲取basePackage的路徑。這樣就完成了定位的過程。(先不討論SpringBoot中指定掃描包路徑和自動裝配)

  2、第二個過程:BeanDefinition的載入

  這個載入過程是把用戶定義好的Bean表示成IoC容器內部的數據結構,而這個容器內部的數據結構就是BeanDefinition。

  在SpringBoot中,上面我們說到通過主類找到了basePackage,SpringBoot會將該路徑拼接成:classpath*:org/springframework/boot/demo/**/*.class這樣的形式,然後一個叫做PathMatchingResourcePatternResolver的類會將該路徑下所有的.class文件都加載進來,然後遍歷判斷是不是有@Component註解,如果有的話,就是我們要裝載的BeanDefinition。大致過程就是這樣的了。

  注意:@Configuration,@Controller,@Service等註解底層都是@Component註解,只不過包裝了一層罷了。

   3、第三個過程:註冊BeanDefinition

   這個過程通過調用上文提到的BeanDefinitionRegister接口的實現來完成。這個註冊過程把載入過程中解析得到的BeanDefinition向IoC容器進行註冊。通過上文的分析,我們可以看到,在IoC容器中將BeanDefinition注入到一個ConcurrentHashMap中,IoC容器就是通過這個HashMap來持有這些BeanDefinition數據的。比如DefaultListableBeanFactory 中的beanDefinitionMap屬性。

六、IoC容器的依賴注入

  上面對IoC容器的初始化過程進行了詳細的介紹,這個過程完成的主要的工作是在IoC容器中建立BeanDefinition數據映射。在此過程中並沒有看到IoC容器對Bean的依賴關係進行注入。依賴注入是Spring實現“控制反轉”的重要一環。Spring將依賴關係交給IoC容器來完成。

  依賴控制反轉的實現有很多種方式。在Spring中,IoC容器是實現這個模式的載體,它可以在對象生成或者初始化時直接將數據注入到對象中,也可以通過將對象注入到對象數據域中的方式來注入對方法調用的依賴。這種依賴注入是可以遞歸的,對象被逐層注入。

 

 

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

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

 

參考文獻:

  《Spring技術內幕–深入解析Spring框架與設計原理(第二版)》

 

【精選推薦文章】

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

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

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

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

Asp.NETCore讓FromServices回來

起因

這两天,我忽然有點懷念 Asp.NET MVC 5 之前的時代,原因是我看到項目裏面有這麼一段代碼(其實不止一段,幾乎每個 Controller 都是)

    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        private readonly IConfiguration configuration;
        private readonly IHostingEnvironment environment;
        private readonly CarService carService;
        private readonly PostServices postServices;
        private readonly TokenService tokenService;
        private readonly TopicService topicService;
        private readonly UserService userService;

        public HomeController(IConfiguration configuration,
                              IHostingEnvironment environment,
                              CarService carService,
                              PostServices postServices,
                              TokenService tokenService,
                              TopicService topicService,
                              UserService userService)
        {
            this.configuration = configuration;
            this.environment = environment;
            this.carService = carService;
            this.postServices = postServices;
            this.tokenService = tokenService;
            this.topicService = topicService;
            this.userService = userService;
        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

在構造函數裏面聲明了一堆依賴注入的實例,外面還得聲明相應的接收字段,使用代碼克隆掃描,零零散散的充斥在各個 Controller 的構造函數中。在 Asp.NET MVC 5 之前,我們可以把上面的代碼簡化為下面的形式:

    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [FromServices] public IConfiguration Configuration { get; set; }
        [FromServices] public IHostingEnvironment Environment { get; set; }
        [FromServices] public CarService CarService { get; set; }
        [FromServices] public PostServices PostServices { get; set; }
        [FromServices] public TokenService TokenService { get; set; }
        [FromServices] public TopicService TopicService { get; set; }
        [FromServices] public UserService UserService { get; set; }

        public HomeController()
        {
        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

但是,在 .NETCore 中,上面的這斷代碼是會報錯的,原因就是特性:FromServicesAttribute 只能應用於 AttributeTargets.Parameter,導航到 FromServicesAttribute 查看源碼

namespace Microsoft.AspNetCore.Mvc
{
    /// <summary>
    /// Specifies that an action parameter should be bound using the request services.
    /// </summary>
    /// <example>
    /// In this example an implementation of IProductModelRequestService is registered as a service.
    /// Then in the GetProduct action, the parameter is bound to an instance of IProductModelRequestService
    /// which is resolved from the request services.
    ///
    /// <code>
    /// [HttpGet]
    /// public ProductModel GetProduct([FromServices] IProductModelRequestService productModelRequest)
    /// {
    ///     return productModelRequest.Value;
    /// }
    /// </code>
    /// </example>
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
    public class FromServicesAttribute : Attribute, IBindingSourceMetadata
    {
        /// <inheritdoc />
        public BindingSource BindingSource => BindingSource.Services;
    }
}

那麼問題來了,AttributeUsage 是什麼時候移除了 AttributeTargets.Property 呢?答案是:2015年11月17日,是一個叫做 Pranav K 的哥們革了 FromServiceAttribute 的命,下面是他的代碼提交記錄

Limit [FromServices] to apply only to parameters
https://github.com/aspnet/Mvc/commit/2a89caed05a1bc9f06d32e15d984cd21598ab6fb

這哥們的 Commit Message 很簡潔:限制 FromServices 僅作用於 parameters 。高手過招,人狠話不多,刀刀致命!從此,廣大 .NETCore 開發者告別了屬性注入。經過我不懈努力的搜索后,發現其實在 Pranav K 提交代碼兩天後,他居然自己開了一個 Issue,你說氣人不?

關於廢除 FromServices 的討論
https://github.com/aspnet/Mvc/issues/3578

在這個貼子裏面,許多開發者表達了自己的不滿,我還看到了有人像我一樣,表達了自己想要一個簡潔的構造函數的這樣樸素的請求;但是,對於屬性注入可能導致濫用的問題也產生了激烈的討論,還有屬性注入要求成員必須標記為 public 這些硬性要求,不得不說,這個帖子成功的引起了人們的注意,但是很明顯,作者不打算修改 FromServices 支持屬性注入。

自己動手,豐衣足食

沒關係,官方沒有自帶的話,我們自己動手做一個也是一樣的效果,在此之前,我們還應該關注另外一種從 service 中獲取實例的方式,就是常見的通過 HttpContext 請求上下文獲取服務實例的方式:

 var obj = HttpContext.RequestServices.GetService(typeof(Type));

上面的這種方式,其實是反模式的,官方也建議盡量避免使用,說完了廢話,就自動動手擼一個屬性注入特性類:PropertyFromServiceAttribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class PropertyFromServiceAttribute : Attribute, IBindingSourceMetadata
{
    public BindingSource BindingSource => BindingSource.Services;
}

沒有多餘的代碼,就是標記為 AttributeTargets.Property 即可

應用到類成員
    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [PropertyFromService] public IConfiguration Configuration { get; set; }
        [PropertyFromService] public IHostingEnvironment Environment { get; set; }
        [PropertyFromService] public CarService CarService { get; set; }
        [PropertyFromService] public PostServices PostServices { get; set; }
        [PropertyFromService] public TokenService TokenService { get; set; }
        [PropertyFromService] public TopicService TopicService { get; set; }
        [PropertyFromService] public UserService UserService { get; set; }

        public HomeController()
        {

        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

請大聲的回答,上面的代碼是不是非常的乾淨整潔!但是,像上面這樣使用屬性注入有一個小問題,在對象未初始化之前,該屬性為 null,意味着在類的構造函數中,該成員變量不可用,不過不要緊,這點小問題完全可用通過在構造函數中注入解決;更重要的是,並非每個實例都需要在構造函數中使用,是吧。

示例代碼

託管在 Github 上了 https://github.com/lianggx/Examples/tree/master/Ron.DI

** 如果你喜歡這篇文章,請給我點贊,讓更多同學可以看到,筆芯~

【精選推薦文章】

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

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

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

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

Spring5源碼深度解析(一)之理解Configuration註解

代碼地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian

1.Spring體繫結構

1.1、Spring Core:主要組件是BeanFactory,創建JavaBean的工廠,使用控制反轉(IOC) 模式  將應用程序的配置和依賴性規範與實際的應用程序代碼分開。

1.2、Spring AOP:集成了面向切面的編程功能(AOP把一個業務流程分成幾部分,例如權限檢查、業務處理、日誌記錄,每個部分單獨處理,然後把它們組裝成完整的業務流程。每個部分被稱為切面),

 可以將聲明性事物管理集成到應用程序中。

1.3、Spring Cntext:一個核心配置文件,為Spring框架提供上下文信息。

1.4、Spring Do:Spring操作數據庫的模塊。

1.5、Spring ORM:Spring集成了各種orm(object relationship mapping 對象關係映射)框架的模塊,集成mybatis

1.6、Spring Web集成各種優秀的web層框架的模塊(Struts、Springmvc)

1.7、Spring web MVC:Spring web層框架

 2.Configuration註解分析內容(@Configuration,@ComponentScan,@Scope,@Lazy)

2.1 @Configuration

 @Configuration用於定義配置類,可替換xml配置文件,被註解的類內部包含有一個或多個被@Bean註解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,並用於構建bean定義,初始化Spring容器。

2.1.1 @Configuration標註在類上,相當於把該類作為spring的xml配置文件中的<beans>,作用為:配置spring容器(應用上下文)

@Configuration
public class MainConfigOfLifeCycle { }

//測試方法

public static void main(String[] args) {
ApplicationContext acac =
new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("ioc容器創建成功");

//關閉ioc容器
((AnnotationConfigApplicationContext) acac).close();
}
 

相當於spring的xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
    xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-5.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-5.0.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-5.0.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-5.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-5.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-5.0.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-5.0.xsd" default-lazy-init="false">


</beans>

2.2 @ComponentScan用法 

 ComponentScan字面意思就是組件掃描,就是根據定義的掃描路徑,把符合掃描規則的類裝配到spring容器中

  2.2.1 ComponentScan參數說明

/*
* @ComponentScan
* value:只當於掃描的的包
* excludeFilters = 指定掃描的時候按照什麼規則排除哪些組件
* includeFilters = 指定掃描的時候只需要包含哪些組件
* Filter.ANNOTATION:按照註解
* Filter.ASSIGNABLE_TYPE: 按照給定的類型
* */

FilterType 有五種類型

ANNOTATION:註解類型

ASSIGNABLE_TYPE:ANNOTATION:指定的類型

ASPECTJ:按照Aspectj的表達式,基本上不會用到

REGEX:按照正則表達式

CUSTOM:自定義規則

package com.brian.config;

import com.brian.bean.Alan;
import com.brian.bean.Brian;
import com.brian.bean.BrianBeanFactory;
import com.brian.bean.Person;
import com.brian.condition.BrianCondition;
import com.brian.condition.BrianSelector;
import com.brian.service.BookService;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Controller;

@Configuration //告訴spring這是一個配置類
/*
* @ComponentScan
*   value:只當於掃描的的包
*   excludeFilters = 指定掃描的時候按照什麼規則排除哪些組件
*   includeFilters = 指定掃描的時候只需要包含哪些組件
*   Filter.ANNOTATION:按照註解
*   Filter.ASSIGNABLE_TYPE: 按照給定的類型
* */

@ComponentScans(value = {
        @ComponentScan(value = "com.brian",includeFilters = {
//                @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}),
//                @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE,classes = {BookService.class}),
                @ComponentScan.Filter(type = FilterType.CUSTOM,classes = {BrianTypeFilter.class})
        },useDefaultFilters = false)
})
@Import({Brian.class,Alan.class,BrianSelector.class})
public class MainConfig {

    @Bean("person") //給容器中註冊一個Bean;類型為返回值的類型;id默認是方法名作為id
    public Person person(){
        return new Person("Alan",18);
    }


    /*
    * @Conditional() 按照條件註冊
    *
    * */
    @Conditional({BrianCondition.class})
    @Bean("person01")
    public Person person01() {
        return new Person("Brian",17);
    }

    @Conditional({BrianCondition.class})
    @Bean("person02")
    public Person person02() {
        return new Person("wenTao",19);
    }

    /*
    *
    *給容器中註冊組件
    * 1,包掃描+ 組件標註註解(@Controller/@Service/@Repository/@Component)[自己寫的方法]
    * 2, @Bean [導入的第三方包裏面的組件]
    * 3,@Import [快速的給容器導入一個組件]
    *       1.@Import(要導入的組件class)
    *       2.ImportSelector:返回需要導入的組件的全類名數組
    *       3.ImportBeanDefinitionRegistrar: 手動註冊bean到容器
    *  4. 使用Spring提供的FactoryBean
    * */
    @Bean
    public BrianBeanFactory brianBeanFactory() {
        return new BrianBeanFactory();
    }

}

2.3 @Scope

默認情況Spring容器是單例的

singleton單例模式:全局有且僅有一個實例。

prototype原型模式:每次獲取Bean的時候都會有一個新的實例。

request

request表示針對每次請求都會產生一個新的Bean對象,並且該Bean對象僅在當前Http請求內有效。

session

session作用域表示煤氣請求都會產生一個新的Bean對象,並且該Bean僅在當前Http session內有效。

測試@Scopeprototype原型模式

Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    @Scope("prototype")
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }
}

測試類

public class MainTest {
    public static void main(String[] args) {
         /*ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfig.class);*/
         ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
        System.out.println("ioc容器創建成功");
        Alan alan1 =  acac.getBean(Alan.class);
        Alan alan2 =  acac.getBean(Alan.class);
        System.out.println("比較兩個Alan實例: " + (alan1 == alan2));

        //關閉ioc容器
        ((AnnotationConfigApplicationContext) acac).close();
    }
}

2.4 @Lazy

Lazy表示為懶加載,當真正需要引用獲取的時候才會被加載

True 表示為懶加載 false表示為在IOC容器加載的時候被創建。

 

測試@Lazy(false)餓漢模式加載

Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    //@Scope("prototype")
    @Lazy(false)
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }


}

測試類

public class MainTest {
    public static void main(String[] args) {
         /*ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfig.class);*/
         ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
        System.out.println("ioc容器創建成功");
      //  Alan alan1 =  acac.getBean(Alan.class);
       // Alan alan2 =  acac.getBean(Alan.class);
        //System.out.println("比較兩個Alan實例: " + (alan1 == alan2));

        //關閉ioc容器
        ((AnnotationConfigApplicationContext) acac).close();
    }
}

 看下結果會發現在餓漢模式下,即使沒用使用AnnotationConfigApplicationContext.getBean()獲取對象,對象也被加載進了IOC容器

測試@Lazy默認懶加載

 Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    //@Scope("prototype")
    @Lazy
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }


}

測試類保持不表

測試結果中,沒有輸出Alan這個對象創建和銷毀的打印信息

 

【精選推薦文章】

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

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

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

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

大話Spark(8)-源碼之DAGScheduler

DAGScheduler的主要作用有2個:

一、把job劃分成多個Stage(Stage內部并行運行,整個作業按照Stage的順序依次執行)
二、提交任務

以下分別介紹下DAGScheduler是如何做這2件事情的,然後再跟源碼看下DAGScheduler的實現。

一、如何把Job劃分成多個Stage

1) 回顧下寬依賴和窄依賴

窄依賴:父RDD的每個分區只被子RDD的一個分區使用。(map,filter,union操作等)
寬依賴:父RDD的分區可能被多個子RDD的分區使用。(reduceByKey,groupByKey等)

如下圖所示,左側的算子為窄依賴, 右側為寬依賴


窄依賴可以支持在同一個集群Executor上,以管道形式順序執行多條命令,例如在執行了map后,緊接着執行filter。分區內的計算收斂,不需要依賴所有分區的數據,可以并行地在不同節點進行計算。所以它的失敗回復也更有效,因為它只需要重新計算丟失的parent partition即可。最重要的是窄依賴沒有shuffle過程,而寬依賴由於父RDD的分區可能被多個子RDD的分區使用,所以一定伴隨着shuffle操作。

2) DAGScheduler 如何把job劃分成多個Stage

DAGScheduler會把job劃分成多個Stage,如下圖sparkui上的截圖所示,job 0 被劃分成了3個stage

DAGScheduler劃分Stage的過程如下:
DAGScheduler會從觸發action操作的那個RDD開始往前倒推,首先會為最後一個RDD創建一個stage,然後往前倒推的時候,如果發現對某個RDD是寬依賴(產生Shuffle),那麼就會將寬依賴的那個RDD創建一個新的stage,那個RDD就是新的stage的最後一個RDD。然後依次類推,繼續往前倒推,根據窄依賴或者寬依賴進行stage的劃分,直到所有的RDD全部遍歷完成為止。

3) wordcount的Stage劃分

在前面大話spark(3)-一圖深入理解WordCount程序在Spark中的執行過程中,我畫過一張wordcount作業的Stage的劃分的圖,如下:

可以看出上圖中,第一個stage的3個task并行執行,遇到reduceByKey這個產生shuffle的操作開始劃分出新的Stage。但是其實這張圖是不準確的。
其實對於每一種有shuffle的操作,比如groupByKey、reduceByKey、countByKey的底層都對應了三個RDD:MapPartitionsRDD、ShuffleRdd、MapPartitionsRDD
(寬依賴shuffle生成的rdd為ShuffleRdd)
其中Shuffle發生在第一個RDD和第二個RDD之間,前面說過如果發現對某個RDD是寬依賴(產生Shuffle),那麼就會將寬依賴的那個RDD創建一個新的stage
所以說上圖中 reduceByKey操作其實對應了3個RDD,其中第一個RDD會被劃分到Stage1中!

4) DAGScheduler劃分Stage源碼

RDD類中所有的action算子觸發計算都會調用sc.runjob方法, 而sc.runjob方法底層都會調用到SparkContext中的dagscheduler對象的runJob方法
例如count這個action操作
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

一直追着runJob方法往底層看最終調用dagScheduler.runJob,傳入調用這個方法的rdd

dagScheduler.runJob內部調用submitJob提交當前的action到scheduler
submitJob內部調用DAGSchedulerEventProcessLoop發送JobSubmitted的信息,
在JobSubmitted內部最終調用dagScheduler的handleJobSubmitted(dagScheduler的核心入口)。

handleJobSubmitted方法如下:

上面代碼中submitStage提交作業,其內代碼如下:

submitStage方法中調用getMissingParentStages方法獲取finalStage的父stage,
如果不存在,則使用submitMissingTasks方法提交執行;
如果存在,則把該stage放到waitingStages中,同時遞歸調用submitStage。通過該算法把存在父stage的stage放入waitingStages中,不存在的作為作業運行的入口。

其中最重要的getMissingParentStages中是stage劃分的核心代碼,如下:

這裏就是前面說到的stage劃分的方式,查看最後一個rdd的依賴,如果是窄依賴,則不創建新的stage,如果是寬依賴,則用getOrCreateShuffledMapStage方法創建新的rdd,依次往前推。

所以Stage的劃分算法最核心的兩個方法為submitStage何getMissingParentStage

二、提交任務

當Stage提交運行后,在DAGScheduler的submitMissingTasks方法中,會根據Stage的Partition個數拆分對應個數任務,這些任務組成一個TaskSet提交到TaskScheduler進行處理。
對於ResultStage(最後一個Stage)生成ResultTask,對於ShuffleMapStage生成ShuffleMapTask。
每一個TaskSet包含對應Stage的所有task,這些Task的處理邏輯完全一樣,不同的是對應處理的數據,而這些數據是對應其數據分片的(Partition)。
submitMissingTasks如下:

【精選推薦文章】

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

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

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

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

【拆分版】Docker-compose構建Logstash多實例,基於7.1.0

【拆分版】Docker-compose構建Logstash多實例

寫在最前

說起Logstash,這個組件並沒有什麼集群的概念,與其說是集群,不如說是各自去收集日誌分析過濾存儲到Elasticsearch中。這裏做個多實例的Logstash,其實本質上只是為Logstash指定好佔用的端口,輸入輸出的配置的掛載,如是而已。

本文配置為紅框中的部分:Logstash多節點收集的數據,統統輸出數據到es-tribe,讓這個協調節點自己去負載均衡寫入數據。

配置詳見git倉庫 https://github.com/hellxz/docker-logstash-multiple.git
如有疑問或本文寫得有出入的地方,期望評論指定。

目錄結構

├── docker-ls-multiple-down.sh
├── docker-ls-multiple-up.sh
├── logstash-01
│   ├── config
│   │   ├── logstash.conf
│   │   └── logstash.yml
│   ├── docker-compose.yml
│   └── .env
├── logstash-02
│   ├── config
│   │   ├── logstash.conf
│   │   └── logstash.yml
│   ├── docker-compose.yml
│   └── .env
└── logstash-03
    ├── config
    │   ├── logstash.conf
    │   └── logstash.yml
    ├── docker-compose.yml
    └── .env

文件說明

logstash-01舉例說明

.envdocker-compose.yml提供了Logstash配置文件目錄的位置,如果不放置到其他位置,無需更改

# .env file for docker-compose default. please be careful.
# logstash config dir mount set. change inside dir config file to change logstash cluster settings.
# default use relation path. don't change if you don't know what means.
LOGSTASH_CONFIG_DIR=./config

docker-compose.yml 為docker-compose的配置文件,這裏只讀取了.env的配置文件的路徑,並把路徑下的logstash.conf掛載到容器中logstash目錄下pipeline/logstash.conf,掛載logstash.yml到logstash目錄下config/logstash.yml

version: "3"
services:
    logstash-1:
        image: logstash:7.1.0
        container_name: logstash-1
        volumes:
            - ${LOGSTASH_CONFIG_DIR}/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:rw
            - ${LOGSTASH_CONFIG_DIR}/logstash.yml:/usr/share/logstash/config/logstash.yml:rw
        network_mode: "host"

logstash.conf為模板文件,輸入輸出以及配置都可以在這裏修改

input {     #輸入
  kafka {   #使用kafka方式輸入
    bootstrap_servers => "kafka1:9092,kafka2:9093,kafka3:9094" #kafka集群節點列表
    topics => ["all_logs"] #訂閱名為all_logs的topic
    group_id => "logstash" #設置組為logstash
    codec => json #轉換為json
  }
}

filter { #過濾分詞等都在這裏配置,暫時未配置

}

output {     #輸出
  elasticsearch { #輸出到es
    hosts => ["10.2.114.110:9204"] #es的路徑
    index => "all-logs-%{+YYYY.MM.dd}" #輸出到es的索引名稱,這裡是每天一個索引
    #user => "elastic"
    #password => "changeme"
  }
  stdout {
    codec => rubydebug
  }
}

此處設置並不是本文中的重點,有興趣和需要請參考其它文章的相關配置

logstash.yml 為logstash的配置文件,只寫了些與集群相關的,還有更多請參考其它文章.

# set now host ip to http.host
http.host: 10.2.114.110
# set the es-tribe-node host. let logstash monitor the es.
xpack.monitoring.elasticsearch.hosts:
- http://10.2.114.110:9204
# enable or disable the logstash monitoring the es.
xpack.monitoring.enabled: true

這裏沒有指定Logstash啟動時的端口號,Logstash默認端口為9600,多實例在同主機時,會自動分配9600后的端口
另外兩個腳本文件,僅在使用同一台主機時使用,便捷啟動/關閉多節點Logstash

使用說明

  1. 需要確保多台主機均能正常ping通
  2. 確保Zookeeper集群與Kafka集群已經啟動,並且Logstash訂閱的borkers的列表能對得起來
  3. 確保Elasticsearch集群正常啟動
  4. 宿主機/etc/hosts添加kafka1kafka2kafka3映射到對應的kafka所在的宿主機ip
  5. 修改每個Logstash目錄下的config/logstash.conf中的輸出es部分的ip到es-tribe對應的宿主機ip
  6. 修改每個Logstash目錄下的config/logstash.yml中的http.host為當前宿主機ip, 修改xpack.monitoring.elasticsearch.hosts為當前es-tribe宿主機ip與port
  7. 進入每個Logstash目錄執行docker-compose up -d以啟動集群,執行docker-compose down以關閉集群

本文系原創文章,謝絕轉載

【精選推薦文章】

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

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

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

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

JavaScript系列–JavaScript數組高階函數reduce()方法詳解及奇淫技巧

一、前言

reduce() 方法接收一個函數作為累加器,數組中的每個值(從左到右)開始縮減,最終計算為一個值。

reduce() 可以作為一個高階函數,用於函數的 compose。

reduce()方法可以搞定的東西,for循環,或者forEach方法有時候也可以搞定,那為啥要用reduce()?這個問題,之前我也想過,要說原因還真找不到,唯一能找到的是:通往成功的道路有很多,但是總有一條路是最捷徑的,亦或許reduce()逼格更高。

 

二、語法

arr.reduce(callback,initialValue)

返回最後一個值,reduce 為數組中的每一個元素依次執行回調函數,不包括數組中被刪除或從未被賦值的元素,接受四個參數:初始值(或者上一次回調函數的返回值),當前元素值,當前索引,調用 reduce 的數組。

 

三、實例解析intialValue參數

1、第一個例子:

var arr = [1, 2, 3, 4]; var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; }) console.log(arr, sum);

打印結果:
1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10

 

2、第二個例子

var arr = [1, 2, 3, 4]; var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; },0) //注意這裏設置了初始值 console.log(arr, sum);

打印結果:
0 1 0
1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10

這個例子index是從0開始的,第一次的prev的值是我們設置的初始值0,數組長度是4,reduce函數循環4次。

結論:如果沒有提供initialValue,reduce 會從索引1的地方開始執行 callback 方法,跳過第一個索引。如果提供initialValue,從索引0開始。

 

注意:如果這個數組為空,運用reduce是什麼情況?

var arr = []; var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; }) //報錯,"TypeError: Reduce of empty array with no initial value"

但是要是我們設置了初始值就不會報錯,如下:

var arr = []; var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; },0) console.log(arr, sum); // [] 0

所以一般來說,提供初始值更加安全。

 

四、reduce簡單用法

當然最簡單的就是我們常用的數組求和,求乘積了。

var arr = [1, 2, 3, 4]; var sum = arr.reduce((x,y)=>x+y) var mul = arr.reduce((x,y)=>x*y) console.log( sum ); //求和,10 console.log( mul ); //求乘積,24

 

五、reduce高級用法

(1)計算數組中每個元素出現的次數

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']; let nameNum = names.reduce((pre,cur)=>{ if(cur in pre){ pre[cur]++ }else{ pre[cur] = 1 } return pre },{}) console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

 

(2)數組去重

let arr = [1,2,3,4,4,1] let newArr = arr.reduce((pre,cur)=>{ if(!pre.includes(cur)){ return pre.concat(cur) }else{ return pre } },[]) console.log(newArr);// [1, 2, 3, 4]

 

(3)將二維數組轉化為一維

let arr = [[0, 1], [2, 3], [4, 5]] let newArr = arr.reduce((pre,cur)=>{ return pre.concat(cur) },[]) console.log(newArr); // [0, 1, 2, 3, 4, 5]

 

(4)將多維數組轉化為一維

let arr = [[0, 1], [2, 3], [4,[5,6,7]]] const newArr = function(arr){ return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[]) } console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

 

(5)對象里的屬性求和

var result = [ { subject: 'math', score: 10 }, { subject: 'chinese', score: 20 }, { subject: 'english', score: 30 } ]; var sum = result.reduce(function(prev, cur) { return cur.score + prev; }, 0); console.log(sum) //60

 

(6)將[1,3,1,4]轉為数字1314

function addDigitValue(prev,curr,curIndex,array){ var exponent = (array.length -1) -curIndex; var digitValue = curr*Math.pow(10,exponent); return prev + digitValue; } var arr6 = [1,3,1,4]; var result7 = arr6.reduce(addDigitValue,0) console.info('result7',result7)

 

【精選推薦文章】

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

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

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

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

技術境界的二三四

兩種能力境界

1.解決問題

在工程師中有一種人被稱為”救火隊長“。哪裡出了問題,哪裡就有他的身影,他的出現,燃眉之急就有救了。他們是解決問題的高人。但是“救火隊長”在晉陞上往往會遇到瓶頸。

對標人物:漫威-美國隊長

每天嚴陣以待,隨時準備拯救世界。無法接受鋼鐵俠防患於未然用機器來解決問題解放自己的方式。

 

2.發現問題

更高的高人會問一個問題:“為什麼每天會任務追着你跑?你為什麼沒從根源上解決所有的問題?”一個在辦公室里和下面人一起研究茶道的領導要比和大家一起加班到半夜的領導受歡迎。因為他們從更大的層面上杜絕了對救火隊長的需要。

對標人物:《罪惡黑名單》雷丁頓

雷丁頓總是運籌帷幄游刃有餘。所以在形勢危急的情況下,他總是評價哪家的什麼東西好吃,或者任何別人沒有注意到的生活瑣事,觀眾並不恨他。因為知道他早就搞定了一切。

 

 

三種需求對應境界

1.對應需求

一個兢兢業業的工程師或團隊,對產品有求必應。項目初期這樣沒有錯,時間一長,就會遇到維護和擴展性問題。

對標:下圖的狀態有木有很熟悉的趕腳?

 

2.快速對應需求

通過系統性的設計和不斷的迭代重構,一個需求來了,通過少量開發或者不開發就可以完成。每周上班五天,三天用來團建。很好,直到公司創始人完成了最初的宏圖偉業,連高層也不知道要干什麼,公司開始走下坡了。

對標:請參考《浪潮之巔》

 

3.引領需求

在線上跑着的服務就會產生數據,通過數據的分析,自己的觀察思考,推演出新的商機和需求,開拓更大的市場。

對標:請參考google的7-2-1原則。

 

 

四種技術運用境界

1.會用

很多面試者在面試中被淘汰時很不服氣,這些我會用,給我分配的活我都干出來了。為什麼不要我?答案很簡單,你這個工作別人也能幹。所以聰明的老闆寧願花4個人的錢招聘3個人干5個人的活。所以怎麼才能獲得一份收入不錯的工作?

2.知道各種優劣勢,知道怎麼用更好

公司絕對不會鼓勵重複造輪子,他們更鼓勵用好輪子。所以深入透徹的技術調研分析,根據場景選擇了合適的技術是個不錯的開始。但是現有的技術並不是為自己定製的。當自己用的足夠深,就發現很多方面,現有技術確實不能滿足自己的業務需要。

3.理解原理及技術血緣,深入運用

特別是一些新技術,由於場景覆蓋還不是很全面,需要在此基礎上做一些二次開發或者內部改造,甚至重寫。重寫重寫着,突然覺得自己有更好的想法?

4.創造技術

技術創造價值,技術引領一個時代。

 

總結

持續有聲音 

 

近期文章

代碼榮辱觀-以運用風格為榮,以隨意編碼為恥

你看不懂的spring原理是因為不知道這幾個概念

應屆生offer指南

【精選推薦文章】

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

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

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

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

高級Java工程師必備 —– 深入分析 Java IO (三)

概述

Java IO即Java 輸入輸出系統。不管我們編寫何種應用,都難免和各種輸入輸出相關的媒介打交道,其實和媒介進行IO的過程是十分複雜的,這要考慮的因素特別多,比如我們要考慮和哪種媒介進行IO(文件、控制台、網絡),我們還要考慮具體和它們的通信方式(順序、隨機、二進制、按字符、按字、按行等等)。Java類庫的設計者通過設計大量的類來攻克這些難題,這些類就位於java.io包中。

在JDK1.4之後,為了提高Java IO的效率,Java又提供了一套新的IO,Java New IO簡稱Java NIO。它在標準java代碼中提供了高速的面向塊的IO操作。本篇文章重點介紹Java IO,關於Java NIO請參考我的另兩篇文章: 

高級Java工程師必備 —– 深入分析 Java IO (一)BIO

高級Java工程師必備 —– 深入分析 Java IO (二)NIO

Java IO類庫的框架

首先看個圖:

 Java IO的類型

雖然java IO類庫龐大,但總體來說其框架還是很清楚的。從是讀媒介還是寫媒介的維度看,Java IO可以分為:

  1. 輸入流:InputStream和Reader
  2. 輸出流:OutputStream和Writer

而從其處理流的類型的維度上看,Java IO又可以分為:

  1. 字節流:InputStream和OutputStream
  2. 字符流:Reader和Writer

下面這幅圖就清晰的描述了JavaIO的分類:

字節流 字符流
輸入流 InputStream Reader
輸出流 OutputStream Writer

我們的程序需要通過InputStream或Reader從數據源讀取數據,然後用OutputStream或者Writer將數據寫入到目標媒介中。其中,InputStream和Reader與數據源相關聯,OutputStream和writer與目標媒介相關聯。

Java IO的基本用法

Java IO :字節流

通過上面的介紹我們已經知道,字節流對應的類應該是InputStream和OutputStream,而在我們實際開發中,我們應該根據不同的媒介類型選用相應的子類來處理。下面我們就用字節流來操作文件媒介:

例1,用字節流寫文件

public static void writeByteToFile() throws IOException{
    String hello= new String( "hello word!");
     byte[] byteArray= hello.getBytes();
    File file= new File( "d:/test.txt");
     //因為是用字節流來寫媒介,所以對應的是OutputStream 
     //又因為媒介對象是文件,所以用到子類是FileOutputStream
    OutputStream os= new FileOutputStream( file);
     os.write( byteArray);
     os.close();
}

例2,用字節流讀文件

public static void readByteFromFile() throws IOException{
    File file= new File( "d:/test.txt");
     byte[] byteArray= new byte[( int) file.length()];
     //因為是用字節流來讀媒介,所以對應的是InputStream
     //又因為媒介對象是文件,所以用到子類是FileInputStream
    InputStream is= new FileInputStream( file);
     int size= is.read( byteArray);
    System. out.println( "大小:"+size +";內容:" +new String(byteArray));
     is.close();
}

CopyFileDemo

package com.chenhao.io.byteIO;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * @author ChenHao
 *
 */
public class CopyFileDemo {

    /**
     * @param args
     * @throws FileNotFoundException 
     */
    public static void main(String[] args) {
        String src ="E:/xp/test";
        String dest="e:/xp/test/4.jpg";
        try {
            copyFile(src,dest);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件不存在");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("拷貝文件失敗|關閉流失敗");
        }
    }
    /**
     * 文件的拷貝
     * @param  源文件路徑
     * @param  目錄文件路徑
     * @throws FileNotFoundException,IOException
     * @return 
     */
    public static void copyFile(String srcPath,String destPath) throws FileNotFoundException,IOException {
        //1、建立聯繫 源(存在且為文件) +目的地(文件可以不存在)  
        File src =new File(srcPath);
        File dest =new File(destPath);
        if(! src.isFile()){ //不是文件或者為null
            System.out.println("只能拷貝文件");
            throw new IOException("只能拷貝文件");
        }
        //2、選擇流
        InputStream is =new FileInputStream(src);
        OutputStream os =new FileOutputStream(dest);
        //3、文件拷貝   循環+讀取+寫出
        byte[] flush =new byte[1024];
        int len =0;
        //讀取
        while(-1!=(len=is.read(flush))){
            //寫出
            os.write(flush, 0, len);
        }
        os.flush(); //強制刷出
        
        //關閉流
        os.close();
        is.close();
    }

}

Java IO :字符流

同樣,字符流對應的類應該是Reader和Writer。下面我們就用字符流來操作文件媒介:

例3,用字符流讀文件

public static void writeCharToFile() throws IOException{
    String hello= new String( "hello word!");
    File file= new File( "d:/test.txt");
    //因為是用字符流來讀媒介,所以對應的是Writer,又因為媒介對象是文件,所以用到子類是FileWriter
    Writer os= new FileWriter( file);
    os.write( hello);
    os.close();
}

例4,用字符流寫文件

public static void readCharFromFile() throws IOException{
    File file= new File( "d:/test.txt");
    //因為是用字符流來讀媒介,所以對應的是Reader
    //又因為媒介對象是文件,所以用到子類是FileReader
    Reader reader= new FileReader( file);
    char [] byteArray= new char[( int) file.length()];
    int size= reader.read( byteArray);
    System. out.println( "大小:"+size +";內容:" +new String(byteArray));
    reader.close();
}

Java IO :字節流轉換為字符流

字節流可以轉換成字符流,java.io包中提供的InputStreamReader類就可以實現,當然從其命名上就可以看出它的作用。其實這涉及到另一個概念,IO流的組合,後面我們詳細介紹。下面看一個簡單的例子:

例5 ,字節流轉換為字符流

public static void convertByteToChar() throws IOException{
    File file= new File( "d:/test.txt");
    //獲得一個字節流
    InputStream is= new FileInputStream( file);
    //把字節流轉換為字符流,其實就是把字符流和字節流組合的結果。
    Reader reader= new InputStreamReader( is);
    char [] byteArray= new char[( int) file.length()];
    int size= reader.read( byteArray);
    System. out.println( "大小:"+size +";內容:" +new String(byteArray));
    is.close();
    reader.close();
}

Java IO:文件媒介操作

例6 ,File操作

public class FileDemo {
  public static void main(String[] args) {
         //檢查文件是否存在
        File file = new File( "d:/test.txt");
         boolean fileExists = file.exists();
        System. out.println( fileExists);
         //創建文件目錄,若父目錄不存在則返回false
        File file2 = new File( "d:/fatherDir/subDir");
         boolean dirCreated = file2.mkdir();
        System. out.println( dirCreated);
         //創建文件目錄,若父目錄不存則連同父目錄一起創建
        File file3 = new File( "d:/fatherDir/subDir2");
         boolean dirCreated2 = file3.mkdirs();
        System. out.println( dirCreated2);
        File file4= new File( "d:/test.txt");
         //判斷長度
         long length = file4.length();
         //重命名文件
         boolean isRenamed = file4.renameTo( new File("d:/test2.txt"));
         //刪除文件
         boolean isDeleted = file4.delete();
        File file5= new File( "d:/fatherDir/subDir");
         //是否是目錄
         boolean isDirectory = file5.isDirectory();
         //列出文件名
        String[] fileNames = file5.list();
         //列出目錄
        File[]   files = file4.listFiles();
  }
}

隨機讀取File文件

通過上面的例子我們已經知道,我們可以用FileInputStream(文件字符流)或FileReader(文件字節流)來讀文件,這兩個類可以讓我們分別以字符和字節的方式來讀取文件內容,但是它們都有一個不足之處,就是只能從文件頭開始讀,然後讀到文件結束。

但是有時候我們只希望讀取文件的一部分,或者是說隨機的讀取文件,那麼我們就可以利用RandomAccessFile。RandomAccessFile提供了seek()方法,用來定位將要讀寫文件的指針位置,我們也可以通過調用getFilePointer()方法來獲取當前指針的位置,具體看下面的例子:

例7,隨機讀取文件

public static void randomAccessFileRead() throws IOException {
     // 創建一個RandomAccessFile對象
    RandomAccessFile file = new RandomAccessFile( "d:/test.txt", "rw");
     // 通過seek方法來移動讀寫位置的指針
     file.seek(10);
     // 獲取當前指針
     long pointerBegin = file.getFilePointer();
     // 從當前指針開始讀
     byte[] contents = new byte[1024];
     file.read( contents);
     long pointerEnd = file.getFilePointer();
    System. out.println( "pointerBegin:" + pointerBegin + "\n" + "pointerEnd:" + pointerEnd + "\n" + new String(contents));
     file.close();
}

例8,隨機寫入文件

public static void randomAccessFileWrite() throws IOException {
     // 創建一個RandomAccessFile對象
     RandomAccessFile file = new RandomAccessFile( "d:/test.txt", "rw");
     // 通過seek方法來移動讀寫位置的指針
     file.seek(10);
     // 獲取當前指針
     long pointerBegin = file.getFilePointer();
     // 從當前指針位置開始寫
     file.write( "HELLO WORD".getBytes());
     long pointerEnd = file.getFilePointer();
     System. out.println( "pointerBegin:" + pointerBegin + "\n" + "pointerEnd:" + pointerEnd + "\n" );
     file.close();
}

Java IO:BufferedInputStream和BufferedOutputStream

BufferedInputStream顧名思義,就是在對流進行寫入時提供一個buffer來提高IO效率。在進行磁盤或網絡IO時,原始的InputStream對數據讀取的過程都是一個字節一個字節操作的,而BufferedInputStream在其內部提供了一個buffer,在讀數據時,會一次讀取一大塊數據到buffer中,這樣比單字節的操作效率要高的多,特別是進程磁盤IO和對大量數據進行讀寫的時候,能提升IO性能。

使用BufferedInputStream十分簡單,只要把普通的輸入流和BufferedInputStream組合到一起即可。我們把上面的例2改造成用BufferedInputStream進行讀文件,請看下面例子:

例10 ,用緩衝流讀文件

public static void readByBufferedInputStream() throws IOException {
     File file = new File( "d:/test.txt");
     byte[] byteArray = new byte[( int) file.length()];
     //可以在構造參數中傳入buffer大小
     InputStream is = new BufferedInputStream( new FileInputStream(file),2*1024);
     int size = is.read( byteArray);
     System. out.println( "大小:" + size + ";內容:" + new String(byteArray));
     is.close();
}

BufferedOutputStream的情況和BufferedInputStream一致,在這裏就不多做描述了。

copyFile

package com.chenhao.io.buffered;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
 * 字節流文件拷貝+緩衝流 ,提高性能
 * 緩衝流(節點流)
 * @author ChenHao
 *
 */
public class BufferedByteDemo {

    public static void main(String[] args) {
        String src ="E:/xp/test";
        String dest="e:/xp/test/4.jpg";
        try {
            copyFile(src,dest);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件不存在");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("拷貝文件失敗|關閉流失敗");
        }
    }
    /**
     * 文件的拷貝
     * @param  源文件路徑
     * @param  目錄文件路徑
     * @throws FileNotFoundException,IOException
     * @return 
     */
    public static void copyFile(String srcPath,String destPath) throws FileNotFoundException,IOException {
        //1、建立聯繫 源(存在且為文件) +目的地(文件可以不存在)  
        File src =new File(srcPath);
        File dest =new File(destPath);
        if(! src.isFile()){ //不是文件或者為null
            System.out.println("只能拷貝文件");
            throw new IOException("只能拷貝文件");
        }
        //2、選擇流
        InputStream is =new BufferedInputStream(new FileInputStream(src));
        OutputStream os =new BufferedOutputStream( new FileOutputStream(dest));
        //3、文件拷貝   循環+讀取+寫出
        byte[] flush =new byte[1024];
        int len =0;
        //讀取
        while(-1!=(len=is.read(flush))){
            //寫出
            os.write(flush, 0, len);
        }
        os.flush(); //強制刷出
        
        //關閉流
        os.close();
        is.close();
    }

}

Java IO:BufferedReader和BufferedWriter

BufferedReader、BufferedWriter 的作用基本和BufferedInputStream、BufferedOutputStream一致,具體用法和原理都差不多 ,只不過一個是面向字符流一個是面向字節流。同樣,我們將改造字符流中的例4,給其加上buffer功能,看例子:

public static void readByBufferedReader() throws IOException {
     File file = new File( "d:/test.txt");
     // 在字符流基礎上用buffer流包裝,也可以指定buffer的大小
     Reader reader = new BufferedReader( new FileReader(file),2*1024);
     char[] byteArray = new char[( int) file.length()];
     int size = reader.read( byteArray);
     System. out.println( "大小:" + size + ";內容:" + new String(byteArray));
     reader.close();
}

另外,BufferedReader提供一個readLine()可以方便地讀取一行,而FileInputStream和FileReader只能讀取一個字節或者一個字符,因此BufferedReader也被稱為行讀取器.

public static void keyIn() throws IOException {
 try (//InputStreamReader是從byte轉成char的橋樑
      InputStreamReader reader = new InputStreamReader(System.in);
      //BufferedReader(Reader in)是char類型輸入的包裝類
      BufferedReader br = new BufferedReader(reader);) {
         
         String line = null;
         while ((line = br.readLine()) != null) {
             if (line.equals("exit")) {
                 //System.exit(1);
                 break;
             }
             System.out.println(line);
         }
     } catch (IOException e) {
         e.printStackTrace();
     }
}

Java IO: 序列化與ObjectInputStream、ObjectOutputStream

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

Serializable

如果你希望類能夠序列化和反序列化,必須實現Serializable接口,就像所展示的ObjectInputStream和ObjectOutputStream例子一樣。

ObjectInputStream

ObjectInputStream能夠讓你從輸入流中讀取Java對象,而不需要每次讀取一個字節。你可以把InputStream包裝到ObjectInputStream中,然後就可以從中讀取對象了。代碼如下:

ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data"));
MyClass object = (MyClass) input.readObject(); //etc.
input.close();

在這個例子中,你讀取的對象必須是MyClass的一個實例,並且必須事先通過ObjectOutputStream序列化到“object.data”文件中。

在你序列化和反序列化一個對象之前,該對象的類必須實現了java.io.Serializable接口。

ObjectOutputStream

ObjectOutputStream能夠讓你把對象寫入到輸出流中,而不需要每次寫入一個字節。你可以把OutputStream包裝到ObjectOutputStream中,然後就可以把對象寫入到該輸出流中了。代碼如下:

ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("object.data"));
MyClass object = new MyClass();  output.writeObject(object); //etc.
output.close();

例子中序列化的對象object現在可以從ObjectInputStream中讀取了。

同樣,在你序列化和反序列化一個對象之前,該對象的類必須實現了java.io.Serializable接口。

 

【精選推薦文章】

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

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

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

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

我的產品/競品分析鍛煉記錄(分析產品核心)

  一來,以前剛入行的時候也想學習下競品分析/產品分析,然後好提高自身的分析能力,當時看了很多文章,然後看到大多都是學生寫的,分析的思路都是從用戶、核心流程,然後一直說到交互,但是整片文章偏重的是說某個小交互怎麼設計得不好,怎麼改進怎麼。

  二來,最近面試了一次,面試我的人應該也是挺教條主義的,面試過程我說了下競品分析,了解過市場行情,當時她追問,我就說下我的分析思路,我的分析思路很特別,只是分析產品核心、整體設計的優缺、預判產品的未來發展方向等,對於細節、業務流程等,一概不分析。

  三來想起來幾年前,那時候我還非常空閑,做了個100個APP分析挑戰,結果沒做完,尷尬。

  所以想整理下這篇,關於我對產品/競品分析的一些思路及想法,以供其他人參考。

 

1、產品經理究竟是負責什麼?

  必須要理清這個點,如果沒對產品經理的職責有清晰定位,很難做出比較適合的分析。為什麼這麼說呢?因為很多剛入行的人,要不從運營/業務入行,要不就技術/畫圖入行,只是了解業務或者功能設計,而不是真正、完整的產品管理。

  產品經理是要對一個產品的從頭到尾的管理,包括用戶了解、分析、業務設計、功能設計實施、運營推廣,從產品冷啟動到產品發展到產品退出市場,產品經理都需要負起帶頭的責任。這個是我理解的產品經理該需要承擔起來的責任。(這裏不展開說,不然這文章沒完沒了)

  基於這個概念,所以我作為一個產品經理,應該關注產品核心價值,因為核心價值才是讓這個產品獨立生存在市場上面,關注市場用戶的情況,而不是將關注點局限在一個小功能、小交互。所以才會有文章一開始說的,分析思路跟那些教科書的,有差異。

2、產品分析與競品分析的差異?

  在我的概念裏面,產品分析是偏重某個產品的的分析,偏重產品的核心價值、核心業務、核心設計。而競品分析偏重是在某個市場環境裏面,同類產品的核心價值差異。

  一個相對深入點,一個相對注重分析面,並且注重市場環境。

3、我的產品分析思路,要分析什麼?

  競品分析就不怎麼寫了,以前我也沒怎麼做過真正的競品分析,產品分析我也是只是分析核心的而已。

  我的方法很原始,就是作為一個用戶、作為一個產品設計者,兩種角色交替去對一個產品進行觀察、感受,寫出它的優缺而已。

  

下面截圖一下當時做的一些內容:

 

 

 

  從上面截圖可以看到當時我做這個鍛煉的目的及原始的一些思路,這個挑戰持續一年的,所以當時在自我學習過程裏面,開始逐漸完善我自身的分析。

  當然,這個是幾年前的分析,分析重點是一個產品的核心,沒有分析細節怎麼設計是好的,怎麼不好,只是純粹從小白的角度去嘗試,從如果我是這個產品的負責人的話,市場環境大概是那樣,我該如何去突破尋求發展?

 

如果說專業一點的競品分析文章,這裏介紹一篇超級詳細的競品分析文章,分析非常深入透徹,但是個人覺得價值不大!為什麼會這麼說呢?

  1、請問要分析一個行業的競品,單靠一個人,要像這位哥們分析到這麼深入,需要多少時間?一個月?一個季度?

  2、以前做產品分析的時候就發現一個問題,當你做完產品分析,產品已經發生改變,你所做的,都是歷史記錄的(當然有很重要的參考意義,這點無可厚非),花費那麼大的時間精力,是否值得?(跟隨是無法超越對方的,因為對方的創新永遠比你快,除非你的創新比它快,這樣要求你的團隊比對方要牛逼,要更清楚用戶需要)

 

  文章鏈接:https://www.zhihu.com/question/23601989/answer/91519343

  作者:大禹

 

       在後來的工作上面,針對這種專門競品/產品分析幾乎是沒有,可能我孤陋寡聞,但是貌似,沒見過有什麼企業,會做比較專門的競品/產品分析,因為做這些實在是耗費時間精力,但是做出來的時候,市場環境可能已經發生了改變了。

       而且當分析深度不夠深的話,還不如外包給專業的市場調研機構,他們會更專業,更有效率產出相對來說更準確的報告。而產品人員通過這些分析,可以拓寬自身眼界,鍛煉思考產品的核心。如果是這樣~~~好像也沒必要畫流程圖啊、寫交互優缺~~

       以上是我個人對產品經理、產品/競品分析的一些見解,純屬個人看法,如有更好的歡迎下面評論一起討論,研究下。

【精選推薦文章】

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

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

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

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