小師妹學JavaIO之:NIO中Channel的妙用

目錄

  • 簡介
  • Channel的分類
    • FileChannel
    • Selector和Channel
    • DatagramChannel
    • SocketChannel
    • ServerSocketChannel
    • AsynchronousSocketChannel
  • 使用Channel
  • 總結

簡介

小師妹,你還記得我們使用IO和NIO的初心嗎?

小師妹:F師兄,使用IO和NIO不就是為了讓生活更美好,世界充滿愛嗎?讓我等程序員可以優雅的將數據從一個地方搬運到另外一個地方。利其器,善其事,才有更多的時間去享受生活呀。

善,如果將數據比做人,IO,NIO的目的就是把人運到美國。

小師妹:F師兄,為什麼要運到美國呀,美國現在新冠太嚴重了,還是待在中國吧。中國是世界上最安全的國家!

好吧,為了保險起見,我們要把人運到上海。人就是數據,怎麼運過去呢?可以坐飛機,坐汽車,坐火車,這些什麼飛機,汽車,火車就可以看做是一個一個的Buffer。

最後飛機的航線,汽車的公路和火車的軌道就可以看做是一個個的channel。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

簡單點講,channel就是負責運送Buffer的通道。

IO按源頭來分,可以分為兩種,從文件來的File IO,從Stream來的Stream IO。不管哪種IO,都可以通過channel來運送數據。

Channel的分類

雖然數據的來源只有兩種,但是JDK中Channel的分類可不少,如下圖所示:

先來看看最基本的,也是最頂層的接口Channel:

public interface Channel extends Closeable {
    public boolean isOpen();
    public void close() throws IOException;

}

最頂層的Channel很簡單,繼承了Closeable接口,需要實現兩個方法isOpen和close。

一個用來判斷channel是否打開,一個用來關閉channel。

小師妹:F師兄,頂層的Channel怎麼這麼簡單,完全不符合Channel很複雜的人設啊。

別急,JDK這麼做其實也是有道理的,因為是頂層的接口,必須要更加抽象更加通用,結果,一通用就發現還真的就只有這麼兩個方法是通用的。

所以為了應對這個問題,Channel中定義了很多種不同的類型。

最最底層的Channel有5大類型,分別是:

FileChannel

這5大channel中,和文件File有關的就是這個FileChannel了。

FileChannel可以從RandomAccessFile, FileInputStream或者FileOutputStream中通過調用getChannel()來得到。

也可以直接調用FileChannel中的open方法傳入Path創建。

public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel

我們看下FileChannel繼承或者實現的接口和類。

AbstractInterruptibleChannel實現了InterruptibleChannel接口,interrupt大家都知道吧,用來中斷線程執行的利器。來看一下下面一段非常玄妙的代碼:

protected final void begin() {
        if (interruptor == null) {
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (closed)
                                return;
                            closed = true;
                            interrupted = target;
                            try {
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }

上面這段代碼就是AbstractInterruptibleChannel的核心所在。

首先定義了一個Interruptible的實例,這個實例中有一個interrupt方法,用來關閉Channel。

然後獲得當前線程的實例,判斷當前線程是否Interrupted,如果是的話,就調用Interruptible的interrupt方法將當前channel關閉。

SeekableByteChannel用來連接Entry或者File。它有一個獨特的屬性叫做position,表示當前讀取的位置。可以被修改。

GatheringByteChannel和ScatteringByteChannel表示可以一次讀寫一個Buffer序列結合(Buffer Array):

public long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;
public long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;

Selector和Channel

在講其他幾個Channel之前,我們看一個和下面幾個channel相關的Selector:

這裏要介紹一個新的Channel類型叫做SelectableChannel,之前的FileChannel的連接是一對一的,也就是說一個channel要對應一個處理的線程。而SelectableChannel則是一對多的,也就是說一個處理線程可以通過Selector來對應處理多個channel。

SelectableChannel通過註冊不同的SelectionKey,實現對多個Channel的監聽。後面我們會具體的講解Selector的使用,敬請期待。

DatagramChannel

DatagramChannel是用來處理UDP的Channel。它自帶了Open方法來創建實例。

來看看DatagramChannel的定義:

public abstract class DatagramChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel

ByteChannel表示它同時是ReadableByteChannel也是WritableByteChannel,可以同時寫入和讀取。

MulticastChannel代表的是一種多播協議。正好和UDP對應。

SocketChannel

SocketChannel是用來處理TCP的channel。它也是通過Open方法來創建的。

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel

SocketChannel跟DatagramChannel的唯一不同之處就是實現的是NetworkChannel借口。

NetworkChannel提供了一些network socket的操作,比如綁定地址等。

ServerSocketChannel

ServerSocketChannel也是一個NetworkChannel,它主要用在服務器端的監聽。

public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel

AsynchronousSocketChannel

最後AsynchronousSocketChannel是一種異步的Channel:

public abstract class AsynchronousSocketChannel
    implements AsynchronousByteChannel, NetworkChannel

為什麼是異步呢?我們看一個方法:

public abstract Future<Integer> read(ByteBuffer dst);

可以看到返回值是一個Future,所以read方法可以立刻返回,只在我們需要的時候從Future中取值即可。

使用Channel

小師妹:F師兄,講了這麼多種類的Channel,看得我眼花繚亂,能不能講一個Channel的具體例子呢?

好的小師妹,我們現在講一個使用Channel進行文件拷貝的例子,雖然Channel提供了transferTo的方法可以非常簡單的進行拷貝,但是為了能夠看清楚Channel的通用使用,我們選擇一個更加常規的例子:

public void useChannelCopy() throws IOException {
        FileInputStream input = new FileInputStream ("src/main/resources/www.flydean.com");
        FileOutputStream output = new FileOutputStream ("src/main/resources/www.flydean.com.txt");
        try(ReadableByteChannel source = input.getChannel(); WritableByteChannel dest = output.getChannel()){
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            while (source.read(buffer) != -1)
            {
                // flip buffer,準備寫入
                buffer.flip();
                // 查看是否有更多的內容
                while (buffer.hasRemaining())
                {
                    dest.write(buffer);
                }
                // clear buffer,供下一次使用
                buffer.clear();
            }
        }
    }

上面的例子中我們從InputStream中讀取Buffer,然後寫入到FileOutputStream。

總結

今天講解了Channel的具體分類,和一個簡單的例子,後面我們會再體驗一下Channel的其他例子,敬請期待。

本文的例子https://github.com/ddean2009/learn-java-io-nio

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/java-io-nio-channel/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

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

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

※教你寫出一流的銷售文案?

談談Spring中的對象跟Bean,你知道Spring怎麼創建對象的嗎?

本系列文章:

讀源碼,我們可以從第一行讀起

你知道Spring是怎麼解析配置類的嗎?

配置類為什麼要添加@Configuration註解?

推薦閱讀:

Spring官網閱讀 | 總結篇

Spring雜談

本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

兩個問題

在開始探討源碼前,我們先思考兩個問題:

1、在Spring中,什麼是Bean?跟對象有什麼區別?

通過new關鍵字,反射,克隆等手段創建出來的就是對象。在Spring中,Bean一定是一個對象,但是對象不一定是一個Bean,一個被創建出來的對象要變成一個Bean要經過很多複雜的工序,例如需要被我們的BeanPostProcessor處理,需要經過初始化,需要經過AOPAOP本身也是由後置處理器完成的)等。

2、在創建對象前,Spring還做了其它什麼事情嗎?

我們還是回到流程圖中,其中相關的步驟如下:

在前面的三篇文章中,我們已經分析到了第3-5步的源碼,而如果你對Spring源碼稍有了解的話,就是知道創建對象以及將對象變成一個Bean的過程發生在第3-11步驟中。中間的五步分別做了什麼呢?

1、registerBeanPostProcessors

就像名字所說的那樣,註冊BeanPostProcessor,這段代碼在Spring官網閱讀(八)容器的擴展點(三)(BeanPostProcessor)已經分析過了,所以在本文就直接跳過了,如果你沒有看過之前的文章也沒有關係,你只需要知道,在這裏Spring將所有的BeanPostProcessor註冊到了容器中

2、initMessageSource

初始化容器中的messageSource,如果程序員沒有提供,默認會創建一個org.springframework.context.support.DelegatingMessageSource,Spring官網閱讀(十一)ApplicationContext詳細介紹(上) 已經介紹過了。

3、initApplicationEventMulticaster

初始化事件分發器,如果程序員沒有提供,那麼默認創建一個org.springframework.context.event.ApplicationEventMulticaster,Spring官網閱讀(十二)ApplicationContext詳解(中)已經做過詳細分析,不再贅述

4、onRefresh

留給子類複寫擴展使用

5、registerListeners

註冊事件監聽器,就是將容器中所有實現了org.springframework.context.ApplicationListener接口的對象放入到監聽器的集合中。

創建對象的源碼分析

在完成了上面的一些準備工作后,Spring開始來創建Bean了,按照流程,首先被調用的就是finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory)方法,我們就以這個方法為入口,一步步跟蹤源碼,看看Spring中的Bean到底是怎麼創建出來的,當然,本文主要關注的是創建對象的這個過程,對象變成Bean的流程我們在後續文章中再分析

1、finishBeanFactoryInitialization

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
   // 初始化一個ConversionService用於類型轉換,這個ConversionService會在實例化對象的時候用到
   if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
         beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
      beanFactory.setConversionService(
            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
   }

  // 添加一個StringValueResolver,用於處理佔位符,可以看到,默認情況下就是使用環境中的屬性值來替代佔位符中的屬性
   if (!beanFactory.hasEmbeddedValueResolver()) {
      beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
   }

   // 創建所有的LoadTimeWeaverAware
   String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
   for (String weaverAwareName : weaverAwareNames) {
      getBean(weaverAwareName);
   }

   // 靜態織入完成后將臨時的類加載器設置為null,所以除了創建LoadTimeWeaverAware時可能會用到臨時類加載器,其餘情況下都為空
   beanFactory.setTempClassLoader(null);

   // 將所有的配置信息凍結
   beanFactory.freezeConfiguration();

   // 開始進行真正的創建
   beanFactory.preInstantiateSingletons();
}

上面的方法最終調用了org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons來創建Bean。

其源碼如下:

2、preInstantiateSingletons

public void  preInstantiateSingletons() throws BeansException {
    	// 所有bd的名稱 
		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
    	// 遍歷所有bd,一個個進行創建 
		for (String beanName : beanNames) {
            // 獲取到指定名稱對應的bd
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            // 對不是延遲加載的單例的Bean進行創建
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                // 判斷是否是一個FactoryBean
				if (isFactoryBean(beanName)) {
                    // 如果是一個factoryBean的話,先創建這個factoryBean,創建factoryBean時,需要在beanName前面拼接一個&符號
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						final FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
											((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
                            // 判斷是否是一個SmartFactoryBean,並且不是懶加載的,就意味着,在創建了這個factoryBean之後要立馬調用它的getObject方法創建另外一個Bean
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
                    // 不是factoryBean的話,我們直接創建就行了
					getBean(beanName);
				}
			}
		}
		// 在創建了所有的Bean之後,遍歷
		for (String beanName : beanNames) {
            // 這一步其實是從緩存中獲取對應的創建的Bean,這裏獲取到的必定是單例的 
			Object singletonInstance = getSingleton(beanName);
            // 判斷是否是一個SmartInitializingSingleton,最典型的就是我們之前分析過的EventListenerMethodProcessor,在這一步完成了對已經創建好的Bean的解析,會判斷其方法上是否有	@EventListener註解,會將這個註解標註的方法通過EventListenerFactory轉換成一個事件監聽器並添加到監聽器的集合中
			if (singletonInstance instanceof SmartInitializingSingleton) {
				final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
				if (System.getSecurityManager() != null) {
					AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
						smartSingleton.afterSingletonsInstantiated();
						return null;
					}, getAccessControlContext());
				}
				else {
					smartSingleton.afterSingletonsInstantiated();
				}
			}
		}
	}

上面這段代碼整體來說應該不難,不過它涉及到了一個點就是factoryBean,如果你對它不夠了解的話,請參考我之前的一篇文章:Spring官網閱讀(七)容器的擴展點(二)FactoryBean

3、doGetBean

從上面的代碼分析中我們可以知道,Spring最終都會調用到getBean方法,而getBean並不是真正幹活的,doGetBean才是。另外doGetBean可以分為兩種情況

  • 創建的是一個FactoryBean,此時實際傳入的name = & + beanName
  • 創建的是一個普通Bean,此時傳入的name = beanName

其代碼如下:

	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
		// 前面我們說過了,傳入的name可能時& + beanName這種形式,這裏做的就是去除掉&,得到beanName
		final String beanName = transformedBeanName(name);
		Object bean;
		// 這個方法就很牛逼了,通過它解決了循環依賴的問題,不過目前我們只需要知道它是從單例池中獲取已經創建的Bean即可,循環依賴後面我單獨寫一篇文章
        // 方法作用:已經創建的Bean會被放到單例池中,這裏就是從單例池中獲取
		Object sharedInstance = getSingleton(beanName);
        
		if (sharedInstance != null && args == null) {
            // 如果直接從單例池中獲取到了這個bean(sharedInstance),我們能直接返回嗎?
            // 當然不能,因為獲取到的Bean可能是一個factoryBean,如果我們傳入的name是 & + beanName 這種形式的話,那是可以返回的,但是我們傳入的更可能是一個beanName,那麼這個時候Spring就還需要調用這個sharedInstance的getObject方法來創建真正被需要的Bean
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}
		else {
            // 在緩存中獲取不到這個Bean
            // 原型下的循環依賴直接報錯
			if (isPrototypeCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(beanName);
			}
            
            // 核心要義,找不到我們就從父容器中再找一次
			BeanFactory parentBeanFactory = getParentBeanFactory();
			if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
				String nameToLookup = originalBeanName(name);
				if (parentBeanFactory instanceof AbstractBeanFactory) {
					return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
							nameToLookup, requiredType, args, typeCheckOnly);
				}
				else if (args != null) {
					return (T) parentBeanFactory.getBean(nameToLookup, args);
				}
				else if (requiredType != null) {
					return parentBeanFactory.getBean(nameToLookup, requiredType);
				}
				else {
					return (T) parentBeanFactory.getBean(nameToLookup);
				}
			}
            
            // 如果不僅僅是為了類型推斷,也就是代表我們要對進行實例化
            // 那麼就將bean標記為正在創建中,其實就是將這個beanName放入到alreadyCreated這個set集合中
			if (!typeCheckOnly) {
				markBeanAsCreated(beanName);
			}
			try {
                
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                
                // 檢查合併后的bd是否是abstract,這個檢查現在已經沒有作用了,必定會通過
				checkMergedBeanDefinition(mbd, beanName, args);

				// @DependsOn註解標註的當前這個Bean所依賴的bean名稱的集合,就是說在創建當前這個Bean前,必須要先將其依賴的Bean先完成創建
				String[] dependsOn = mbd.getDependsOn();
				if (dependsOn != null) {
                    // 遍歷所有申明的依賴
					for (String dep : dependsOn) {
                        // 如果這個bean所依賴的bean又依賴了當前這個bean,出現了循環依賴,直接報錯
						if (isDependent(beanName, dep)) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
						}
                        // 註冊bean跟其依賴的依賴關係,key為依賴,value為依賴所從屬的bean
						registerDependentBean(dep, beanName);
						try {
                            // 先創建其依賴的Bean
							getBean(dep);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
						}
					}
				}
				// 我們目前只分析單例的創建,單例看懂了,原型自然就懂了
				if (mbd.isSingleton()) {
                    // 這裏再次調用了getSingleton方法,這裏跟方法開頭調用的getSingleton的區別在於,這個方法多傳入了一個ObjectFactory類型的參數,這個ObjectFactory會返回一個Bean
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							destroySingleton(beanName);
							throw ex;
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}
		// 省略原型跟域對象的相關代碼
		return (T) bean;
	}

配合註釋看這段代碼應該也不難吧,我們重點關注最後在調用的這段方法即可

4、getSingleton(beanName,ObjectFactory)

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
   Assert.notNull(beanName, "Bean name must not be null");
   synchronized (this.singletonObjects) {
       // 從單例池中獲取,這個地方肯定也獲取不到
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
          // 工廠已經在銷毀階段了,這個時候還在創建Bean的話,就直接拋出異常
         if (this.singletonsCurrentlyInDestruction) {
            throw new BeanCreationNotAllowedException(beanName,
                  "Singleton bean creation not allowed while singletons of this factory are in destruction " +
                  "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
         }
         // 在單例創建前,記錄一下正在創建的單例的名稱,就是把beanName放入到singletonsCurrentlyInCreation這個set集合中去
         beforeSingletonCreation(beanName);
         boolean newSingleton = false;
         boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
         if (recordSuppressedExceptions) {
            this.suppressedExceptions = new LinkedHashSet<>();
         }
         try {
             // 這裏調用了singletonFactory的getObject方法,對應的實現就是在doGetBean中的那一段lambda表達式
            singletonObject = singletonFactory.getObject();
            newSingleton = true;
         }
        // 省略異常處理
         finally {
            if (recordSuppressedExceptions) {
               this.suppressedExceptions = null;
            }
             // 在單例完成創建后,將beanName從singletonsCurrentlyInCreation中移除
             // 標志著這個單例已經完成了創建
            afterSingletonCreation(beanName);
         }
         if (newSingleton) {
             // 添加到單例池中
            addSingleton(beanName, singletonObject);
         }
      }
      return singletonObject;
   }
}

分析完上面這段代碼,我們會發現,核心的創建Bean的邏輯就是在singletonFactory.getObject()這句代碼中,而其實現就是在doGetBean方法中的那一段lambda表達式,如下:

實際就是通過createBean這個方法創建了一個Bean然後返回,createBean又幹了什麼呢?

5、createBean

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {

   RootBeanDefinition mbdToUse = mbd;
    
    // 解析得到beanClass,為什麼需要解析呢?如果是從XML中解析出來的標籤屬性肯定是個字符串嘛
    // 所以這裏需要加載類,得到Class對象
   Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
   if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
      mbdToUse = new RootBeanDefinition(mbd);
      mbdToUse.setBeanClass(resolvedClass);
   }
   // 對XML標籤中定義的lookUp屬性進行預處理,如果只能根據名字找到一個就標記為非重載的,這樣在後續就不需要去推斷到底是哪個方法了,對於@LookUp註解標註的方法是不需要在這裏處理的,AutowiredAnnotationBeanPostProcessor會處理這個註解
   try {
      mbdToUse.prepareMethodOverrides();
   }
   // 省略異常處理...
   try {
       // 在實例化對象前,會經過後置處理器處理
       // 這個後置處理器的提供了一個短路機制,就是可以提前結束整個Bean的生命周期,直接從這裏返回一個Bean
       // 不過我們一般不會這麼做,它的另外一個作用就是對AOP提供了支持,在這裡會將一些不需要被代理的Bean進行標記,就本文而言,你可以暫時理解它沒有起到任何作用
      Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
      if (bean != null) {
         return bean;
      }
   }
    // 省略異常處理...
   try {
       // doXXX方法,真正幹活的方法,doCreateBean,真正創建Bean的方法
      Object beanInstance = doCreateBean(beanName, mbdToUse, args);
      if (logger.isDebugEnabled()) {
         logger.debug("Finished creating instance of bean '" + beanName + "'");
      }
      return beanInstance;
   }
  // 省略異常處理...
}

6、doCreateBean

本文只探討對象是怎麼創建的,至於怎麼從一個對象變成了Bean,在後面的文章我們再討論,所以我們主要就關注下面這段代碼

// 這個方法真正創建了Bean,創建一個Bean會經過 創建對象 > 依賴注入 > 初始化 這三個過程,在這個過程中,BeanPostPorcessor會穿插執行,本文主要探討的是創建對象的過程,所以關於依賴注入及初始化我們暫時省略,在後續的文章中再繼續研究
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {

   // Instantiate the bean.
   BeanWrapper instanceWrapper = null;
   if (mbd.isSingleton()) {
       // 這行代碼看起來就跟factoryBean相關,這是什麼意思呢?
       // 在下文我會通過例子介紹下,你可以暫時理解為,這個地方返回的就是個null
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
   }
   if (instanceWrapper == null) {
       // 這裏真正的創建了對象
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }
   // 省略依賴注入,初始化
}

這裏我先分析下this.factoryBeanInstanceCache.remove(beanName)這行代碼。這裏需要說一句,我寫的這個源碼分析的系列非常的細節,之所以選擇這樣一個個扣細節是因為我自己在閱讀源碼過程中經常會被這些問題阻塞,那麼藉著這些文章將自己踩過的坑分享出來可以減少作為讀者的你自己在閱讀源碼時的障礙,其次也能夠提升自己閱讀源碼的能力。如果你對這些細節不感興趣的話,可以直接跳過,能把握源碼的主線即可。言歸正傳,我們回到這行代碼this.factoryBeanInstanceCache.remove(beanName)。什麼時候factoryBeanInstanceCache這個集合中會有值呢?這裏我還是以示例代碼來說明這個問題,示例代碼如下:

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
	}
}

// 沒有做什麼特殊的配置,就是掃描了需要的組件,測試時換成你自己的包名
@ComponentScan("com.dmz.source.instantiation")
@Configuration
public class Config {
}

// 這裏申明了一個FactoryBean,並且通過@DependsOn註解申明了這個FactoryBean的創建要在orderService之後,主要目的是為了在DmzFactoryBean創建前讓容器發生一次屬性注入
@Component
@DependsOn("orderService")
public class DmzFactoryBean implements FactoryBean<DmzService> {
	@Override
	public DmzService getObject() throws Exception {
		return new DmzService();
	}

	@Override
	public Class<?> getObjectType() {
		return DmzService.class;
	}
}

// 沒有通過註解的方式將它放到容器中,而是通過上面的DmzFactoryBean來管理對應的Bean
public class DmzService {
}

// OrderService中需要注入dmzService
@Component
public class OrderService {
	@Autowired
	DmzService dmzService;
}

在這段代碼中,因為我們明確的表示了DmzFactoryBean是依賴於orderService的,所以必定會先創建orderService再創建DmzFactoryBean,創建orderService的流程如下:

其中的屬性注入階段,我們需要細化,也可以畫圖如下:

為orderService進行屬性注入可以分為這麼幾步

  1. 找到需要注入的注入點,也就是orderService中的dmzService字段

  2. 根據字段的類型以及名稱去容器中查詢符合要求的Bean

  3. 當遍歷到一個FactroyBean時,為了確定其getObject方法返回的對象的類型需要創建這個FactroyBean(只會到對象級別),然後調用這個創建好的FactroyBean的getObjectType方法明確其類型並與注入點需要的類型比較,看是否是一個候選的Bean,在創建這個FactroyBean時就將其放入了factoryBeanInstanceCache中。

  4. 在確定了唯一的候選Bean之後,Spring就會對這個Bean進行創建,創建的過程又經過三個步驟

    • 創建對象
    • 屬性注入
    • 初始化

    在創建對象時,因為此時factoryBeanInstanceCache已經緩存了這個Bean對應的對象,所以直接通過this.factoryBeanInstanceCache.remove(beanName)這行代碼就返回了,避免了二次創建對象。

7、createBeanInstance

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
   
   Class<?> beanClass = resolveBeanClass(mbd, beanName);
   // 省略異常
    
    // 通過bd中提供的instanceSupplier來獲取一個對象
    // 正常bd中都不會有這個instanceSupplier屬性,這裏也是Spring提供的一個擴展點,但實際上不常用
   Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
   if (instanceSupplier != null) {
      return obtainFromSupplier(instanceSupplier, beanName);
   }

   // bd中提供了factoryMethodName屬性,那麼要使用工廠方法的方式來創建對象,工廠方法又會區分靜態工廠方法跟實例工廠方法
   if (mbd.getFactoryMethodName() != null) {
      return instantiateUsingFactoryMethod(beanName, mbd, args);
   }

   // 在原型模式下,如果已經創建過一次這個Bean了,那麼就不需要再次推斷構造函數了
   boolean resolved = false;  // 是否推斷過構造函數
   boolean autowireNecessary = false;  // 構造函數是否需要進行注入
   if (args == null) {
      synchronized (mbd.constructorArgumentLock) {
         if (mbd.resolvedConstructorOrFactoryMethod != null) {
            resolved = true;
            autowireNecessary = mbd.constructorArgumentsResolved;
         }
      }
   }
   if (resolved) {
      if (autowireNecessary) {
         return autowireConstructor(beanName, mbd, null, null);
      }
      else {
         return instantiateBean(beanName, mbd);
      }
   }

   // 推斷構造函數
   Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
   if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
         mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
      return autowireConstructor(beanName, mbd, ctors, args);
   }

   // 調用無參構造函數創建對象
   return instantiateBean(beanName, mbd);
}

上面這段代碼在Spring官網閱讀(一)容器及實例化 已經分析過了,但是當時我們沒有深究創建對象的細節,所以本文將詳細探討Spring中的這個對象到底是怎麼創建出來的,這也是本文的主題。

在Spring官網閱讀(一)容器及實例化 這篇文章中,我畫了下面這麼一張圖

從上圖中我們可以知道Spring在實例化對象的時候有這麼幾種方式

  1. 通過bd中的supplier屬性
  2. 通過bd中的factoryMethodName跟factoryBeanName
  3. 通過構造函數

我們接下來就一一分析其中的細節:

》通過bd中的supplier屬性實例化對象

在Spring官網閱讀(一)容器及實例化 文中介紹過這種方式,因為這種方式我們基本不會使用,並不重要,所以這裏就不再贅述,我這裏就直接給出一個使用示例,大家自行體會吧

public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    // 直接註冊一個Bean,並且指定它的supplier就是Service::new
		ac.registerBean("service", Service.class,Service::new,zhe'sh);
		ac.refresh();
		System.out.println(ac.getBean("service"));
}

》通過bd中的factoryMethodName跟factoryBeanName實例化對象

對應代碼如下:

protected BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    return new ConstructorResolver(this).instantiateUsingFactoryMethod(beanName, mbd, explicitArgs);
}

上面這段代碼主要幹了兩件事

  • 創建一個ConstructorResolver對象,從類名來看,它是一個構造器解析器
  • 調用了這個構造器解析器的instantiateUsingFactoryMethod方法,這個方法見名知意,使用FactoryMethod來完成實例化

基於此,我們解決一個問題,ConstructorResolver是什麼?

ConstructorResolver是什麼?

在要研究一個類前,我們最先應該從哪裡入手呢?很多沒有經驗的同學可能會悶頭看代碼,但是實際上最好的學習方式是先閱讀類上的javaDoc

ConstructorResolver上的javaDoc如下:

上面這段javaDoc翻譯過來就是這個類就是用來解析構造函數跟工廠方法的代理者,並且它是通過參數匹配的方式來進行推斷構造方法或者工廠方法

看到這裏不知道小夥伴們是否有疑問,就是明明這個類不僅負責推斷構造函數,還會負責推斷工廠方法,那麼為什麼類名會叫做ConstructorResolver呢?我們知道Spring的代碼在業界來說絕對是最規範的,沒有之一,這樣來說的話,這個類最合適的名稱應該是ConstructorAndFactoryMethodResolver才對,因為它不僅負責推斷了構造函數還負責推斷了工廠方法嘛!

這裏我需要說一下我自己的理解。對於一個Bean,它是通過構造函數完成實例化的,或者通過工廠方法實例化的,其實在這個Bean看來都沒有太大區別,這兩者都可以稱之為這個Bean的構造器,因為通過它們都能構造出一個Bean。所以Spring就把兩者統稱為構造器了,所以這個類名也就被稱為ConstructorResolver了。

Spring在很多地方體現了這種實現,例如在XML配置的情況下,不論我們是使用構造函數創建對象還是使用工廠方法創建對象,其參數的標籤都是使用constructor-arg。比如下面這個例子

<bean id="dmzServiceGetFromStaticMethod"
      factory-bean="factoryBean"
      factory-method="getObject">
    <constructor-arg type="java.lang.String" value="hello" name="s"/>
    <constructor-arg type="com.dmz.source.instantiation.service.DmzFactory" ref="factoryBean"/>
</bean>

<!--測試靜態工廠方法創建對象-->
<bean id="service"
      class="com.dmz.official.service.MyFactoryBean"
      factory-method="staticGet">
    <constructor-arg type="java.lang.String" value="hello"/>
</bean>

<bean id="dmzService" class="com.dmz.source.instantiation.service.DmzService">
    <constructor-arg name="s" value="hello"/>
</bean>

在對這個類有了大概的了解后,我們就需要來分析它的源碼,這裏我就不把它單獨拎出來分析了,我們藉著Spring的流程看看這個類幹了什麼事情

instantiateUsingFactoryMethod方法做了什麼?

核心目的:推斷出要使用的factoryMethod以及調用這個FactoryMethod要使用的參數,然後反射調用這個方法實例化出一個對象

這個方法的代碼太長了,所以我們將它拆分成為一段一段的來分析

方法參數分析

在分析上面的代碼之前,我們先來看看這個方法的參數都是什麼含義

方法上關於參數的介紹如圖所示

  • beanName:當前要實例化的Bean的名稱
  • mbd:當前要實例化的Bean對應的BeanDefinition
  • explicitArgs:這個參數在容器啟動階段我們可以認定它就是null,只有显示的調用了getBean方法,並且傳入了明確的參數,例如:getBean("dmzService","hello")這種情況下才會不為null,我們分析這個方法的時候就直接認定這個參數為null即可
第一段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    // 第一段代碼:創建並初始話一個BeanWrapperImpl
    BeanWrapperImpl bw = new BeanWrapperImpl();
    this.beanFactory.initBeanWrapper(bw);
    // ......
}

BeanWrapperImpl是什麼呢?如果你看過我之前的文章:Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換,那麼你對這個類應該不會陌生,它就是對Bean進行了一層包裝,並且在創建Bean的時候以及進行屬性注入的時候能夠進行類型轉換。就算你沒看過之前的文章也沒關係,只要記住兩點

  • BeanWrapperImpl包裝了一個實例化好的對象
  • BeanWrapperImpl能夠對屬性進行類型轉換

其層級關係如下:

回到我們的源碼分析,我們先來看看new BeanWrapperImpl()做了什麼事情?

對應代碼如下:

// 第一步:調用空參構造
public BeanWrapperImpl() {
    // 調用另外一個構造函數,表示要註冊默認的屬性編輯器
    this(true);
}

// 這個構造函數表明是否要註冊默認編輯器,上面傳入的值為true,表示需要註冊
public BeanWrapperImpl(boolean registerDefaultEditors) {
    super(registerDefaultEditors);
}

// 調用到父類的構造函數,確定要使用默認的屬性編輯器
protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) {
    if (registerDefaultEditors) {
        registerDefaultEditors();
    }
    // 對typeConverterDelegate進行初始化
    this.typeConverterDelegate = new TypeConverterDelegate(this);
}

總的來說創建的過程非常簡單。第一,確定要註冊默認的屬性編輯器;第二,對typeConverterDelegate屬性進行初始化。

緊接着,我們看看在初始化這個BeanWrapper做了什麼?

// 初始化BeanWrapper,主要就是將容器中配置的conversionService賦值到當前這個BeanWrapper上
// 同時註冊定製的屬性編輯器
protected void initBeanWrapper(BeanWrapper bw) {
    bw.setConversionService(getConversionService());
    registerCustomEditors(bw);
}

還記得conversionService在什麼時候被放到容器中的嗎?就是在finishBeanFactoryInitialization的時候啦~!

conversionService屬性完成賦值后就開始註冊定製的屬性編輯器,代碼如下:

// 傳入的參數就是我們的BeanWrapper,它同時也是一個屬性編輯器註冊表
protected void registerCustomEditors(PropertyEditorRegistry registry) {
    PropertyEditorRegistrySupport registrySupport =
        (registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
    if (registrySupport != null) {
        // 這個配置的作用就是在註冊默認的屬性編輯器時,可以增加對數組到字符串的轉換功能
        // 默認就是通過","來切割字符串轉換成數組,對應的屬性編輯器就是StringArrayPropertyEditor
        registrySupport.useConfigValueEditors();
    }
    // 將容器中的屬性編輯器註冊到當前的這個BeanWrapper
    if (!this.propertyEditorRegistrars.isEmpty()) {
        for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
            registrar.registerCustomEditors(registry);
            // 省略異常處理~
        }
    }
    // 這裏我們沒有添加任何的自定義的屬性編輯器,所以肯定為空
    if (!this.customEditors.isEmpty()) {
        this.customEditors.forEach((requiredType, editorClass) ->
                                   registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass)));
    }
}
第二段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {

    // 省略已經分析的第一段代碼,到這裏已經得到了一個具有類型轉換功能的BeanWrapper
	
    // 實例化這個Bean的工廠Bean
    Object factoryBean;
    // 工廠Bean的Class
    Class<?> factoryClass;
    // 靜態工廠方法或者是實例化工廠方法
    boolean isStatic;
	
    /*下面這段代碼就是為上面申明的這三個屬性賦值*/ 
    String factoryBeanName = mbd.getFactoryBeanName();
    // 如果創建這個Bean的工廠就是這個Bean本身的話,那麼直接拋出異常
    if (factoryBeanName != null) {
        if (factoryBeanName.equals(beanName)) {
            throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                                                   "factory-bean reference points back to the same bean definition");
        }
        // 得到創建這個Bean的工廠Bean
        factoryBean = this.beanFactory.getBean(factoryBeanName);
        if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
            throw new ImplicitlyAppearedSingletonException();
        }
        factoryClass = factoryBean.getClass();
        isStatic = false;
    }
    else {
        // factoryBeanName為null,說明是通過靜態工廠方法來實例化Bean的
        // 靜態工廠進行實例化Bean,beanClass屬性必須要是工廠的class,如果為空,直接報錯
        if (!mbd.hasBeanClass()) {
            throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                                                   "bean definition declares neither a bean class nor a factory-bean reference");
        }
        factoryBean = null;
        factoryClass = mbd.getBeanClass();
        isStatic = true;
    }
    // 省略後續代碼
}

小總結:

這段代碼很簡單,就是確認實例化當前這個Bean的工廠方法是靜態工廠還是實例工廠,如果是實例工廠,那麼找出對應的工廠Bean。

第三段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    
    // 省略第一段,第二段代碼
    
    // 到這裏已經得到了一個BeanWrapper,明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    // 並且已經確定了工廠Bean
    
    // 最終確定的要用來創建對象的方法
    Method factoryMethodToUse = null;
    ArgumentsHolder argsHolderToUse = null;
    Object[] argsToUse = null;
	
    // 參數分析時已經說過,explicitArgs就是null
    if (explicitArgs != null) {
        argsToUse = explicitArgs;
    }
    else {
        // 下面這段代碼是什麼意思呢?
        // 在原型模式下,我們會多次創建一個Bean,所以Spring對參數以及所使用的方法做了緩存
        // 在第二次創建原型對象的時候會進入這段緩存的邏輯
        // 但是這裡有個問題,為什麼Spring對參數有兩個緩存呢?
        // 一:resolvedConstructorArguments
        // 二:preparedConstructorArguments
        // 這裏主要是因為,直接使用解析好的構造的參數,因為這樣會導致創建出來的所有Bean都引用同一個屬性
        Object[] argsToResolve = null;
        synchronized (mbd.constructorArgumentLock) {
            factoryMethodToUse = (Method) mbd.resolvedConstructorOrFactoryMethod;
            // 緩存已經解析過的工廠方法或者構造方法
            if (factoryMethodToUse != null && mbd.constructorArgumentsResolved) {
                // resolvedConstructorArguments跟preparedConstructorArguments都是對參數的緩存
                argsToUse = mbd.resolvedConstructorArguments;
                if (argsToUse == null) {
                    argsToResolve = mbd.preparedConstructorArguments;
                }
            }
        }
        if (argsToResolve != null) {
            // preparedConstructorArguments需要再次進行解析
            argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve);
        }
    }
    // 省略後續代碼
}

小總結:

上面這段代碼應該沒什麼大問題,其核心思想就是從緩存中取已經解析出來的方法以及參數,這段代碼只會在原型模式下生效,因為單例的話對象只會創建一次嘛~!最大的問題在於,為什麼在對參數進行緩存的時候使用了兩個不同的集合,並且緩存后的參數還需要再次解析,這個問題我們暫且放着,不妨帶着這個問題往下看。

因為接下來要分析的代碼就比較複雜了,所以為了讓你徹底看到代碼的執行流程,下面我會使用示例+流程圖+文字的方式來分析源碼。

示例代碼如下(這個例子覆蓋接下來要分析的所有流程):

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
	   default-autowire="constructor"><!--這裏開啟自動注入,並且是通過構造函數進行自動注入-->

	<!--factoryObject 提供了創建對象的方法-->
	<bean id="factoryObject" class="com.dmz.spring.first.instantiation.service.FactoryObject"/>

	<!--提供一個用於測試自動注入的對象-->
	<bean class="com.dmz.spring.first.instantiation.service.OrderService" id="orderService"/>
	
    <!--主要測試這個對象的實例化過程-->
	<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" scope="prototype">
		<constructor-arg name="name" value="dmz"/>
		<constructor-arg name="age" value="18"/>
		<constructor-arg name="birthDay" value="2020-05-23"/>
	</bean>
	
    <!--測試靜態方法實例化對象的過程-->
	<bean id="indexService" class="com.dmz.spring.first.instantiation.service.FactoryObject"
		  factory-method="staticGetIndex"/>
	
    <!--提供這個轉換器,用於轉換dmzService中的birthDay屬性,從字符串轉換成日期對象-->
	<bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="conversionService">
		<property name="converters">
			<set>
				<bean class="com.dmz.spring.first.instantiation.service.ConverterStr2Date"/>
			</set>
		</property>
	</bean>
</beans>

測試代碼:

public class FactoryObject {

	public DmzService getDmz(String name, int age, Date birthDay, OrderService orderService) {
		System.out.println("getDmz with "+"name,age,birthDay and orderService");
		return new DmzService();
	}

	public DmzService getDmz(String name, int age, Date birthDay) {
		System.out.println("getDmz with "+"name,age,birthDay");
		return new DmzService();
	}

	public DmzService getDmz(String name, int age) {
		System.out.println("getDmz with "+"name,age");
		return new DmzService();
	}

	public DmzService getDmz() {
		System.out.println("getDmz with empty arg");
		return new DmzService();
	}

	public static IndexService staticGetIndex() {
		return new IndexService();
	}
}

public class DmzService {
}

public class IndexService {
}

public class OrderService {
}

public class ConverterStr2Date implements Converter<String, Date> {
	@Override
	public Date convert(String source) {
		try {
			return new SimpleDateFormat("yyyy-MM-dd").parse(source);
		} catch (ParseException e) {
			return null;
		}
	}
}

/**
 * @author 程序員DMZ
 * @Date Create in 23:14 2020/5/21
 * @Blog https://daimingzhi.blog.csdn.net/
 */
public class Main {
	public static void main(String[] args) {
		ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext();
		cc.setConfigLocation("application.xml");
		cc.refresh();
		cc.getBean("dmzService");
        // 兩次調用,用於測試緩存的方法及參數
//		cc.getBean("dmzService");

	}
}

運行上面的代碼會發現,程序打印:

getDmz with name,age,birthDay and orderService

具體原因我相信你看了接下來的源碼分析自然就懂了

第四段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
//  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
//  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
//	第三段代碼:以及從緩存中取過了對應了方法以及參數

// 進入第四段代碼分析,執行到這段代碼說明是第一次實例化這個對象
if (factoryMethodToUse == null || argsToUse == null) {
			// 如果被cglib代理的話,獲取父類的class
			factoryClass = ClassUtils.getUserClass(factoryClass);
			// 獲取到工廠類中的所有方法,接下來要一步步從這些方法中篩選出來符合要求的方法
			Method[] rawCandidates = getCandidateMethods(factoryClass, mbd);
			List<Method> candidateList = new ArrayList<>();
    		// 第一步篩選:之前 在第二段代碼中已經推斷了方法是靜態或者非靜態的
    		// 所以這裏第一個要求就是要滿足靜態/非靜態這個條件
    		// 第二個要求就是必須符合bd中定義的factoryMethodName的名稱
    		// 其中第二個要求請注意,如果bd是一個configurationClassBeanDefinition,也就是說是通過掃描@Bean註解產生的,那麼在判斷時還會添加是否標註了@Bean註解
			for (Method candidate : rawCandidates) {
				if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate)) {
					candidateList.add(candidate);
				}
			}
    		// 將之前得到的方法集合轉換成數組
    		// 到這一步得到的其實就是某一個方法的所有重載方法
    	 	// 比如dmz(),dmz(String name),dmz(String name,int age)
			Method[] candidates = candidateList.toArray(new Method[0]);
    
    		// 排序,public跟參數多的優先級越高
			AutowireUtils.sortFactoryMethods(candidates);
			
    		// 用來保存從配置文件中解析出來的參數
			ConstructorArgumentValues resolvedValues = null;
            // 是否使用了自動注入,本段代碼中沒有使用到這個屬性,但是在後面用到了
			boolean autowiring = (mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
			int minTypeDiffWeight = Integer.MAX_VALUE;
    		// 可能出現多個符合要求的方法,用這個集合保存,實際上如果這個集合有值,就會拋出異常了
			Set<Method> ambiguousFactoryMethods = null;

			int minNrOfArgs;
    		// 必定為null,不考慮了
			if (explicitArgs != null) {
				minNrOfArgs = explicitArgs.length;
			}
			else {
                // 就是說配置文件中指定了要使用的參數,那麼需要對其進行解析,解析后的值就存儲在resolvedValues這個集合中
				if (mbd.hasConstructorArgumentValues()) {
                    // 通過解析constructor-arg標籤,將參數封裝成了ConstructorArgumentValues
                    // ConstructorArgumentValues這個類在下文我們專門分析
					ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
					resolvedValues = new ConstructorArgumentValues();
                    // 解析標籤中的屬性,類似進行類型轉換,後文進行詳細分析
					minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
				}
				else {
                    // 配置文件中沒有指定要使用的參數,所以執行方法的最小參數個數就是0
					minNrOfArgs = 0;
				}
			}
	// 省略後續代碼....
}

小總結:

因為在實例化對象前必定要先確定具體要使用的方法,所以這裏先做的第一件事就是確定要在哪個範圍內去推斷要使用的factoryMethod呢?

最大的範圍就是這個factoryClass的所有方法,也就是源碼中的rawCandidates

其次需要在rawCandidates中進一步做推斷,因為在前面第二段代碼的時候已經確定了是靜態方法還是非靜態方法,並且BeanDefinition也指定了factoryMethodName,那麼基於這兩個條件這裏就需要對rawCandidates進一步進行篩選,得到一個candidateList集合。

我們對示例的代碼進行調試會發現

確實如我們所料,rawCandidates是factoryClass中的所有方法,candidateList是所有getDmz的重載方法。

在確定了推斷factoryMethod的範圍后,那麼接下來要根據什麼去確定到底使用哪個方法呢?換個問題,怎麼區分這麼些重載的方法呢?肯定是根據方法參數嘛!

所以接下來要做的就是去解析要使用的參數了~

對於Spring而言,方法的參數會分為兩種

  1. 配置文件中指定的
  2. 自動注入模式下,需要去容器中查找的

在上面的代碼中,Spring就是將配置文件中指定的參數做了一次解析,對應方法就是resolveConstructorArguments

在查看這個方法的源碼前,我們先看看ConstructorArgumentValues這個類

public class ConstructorArgumentValues {
	// 通過下標方式指定的參數
	private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<>();
	// 沒有指定下標
	private final List<ValueHolder> genericArgumentValues = new ArrayList<>();
	// 省略無關代碼.....
}

在前文的註釋中我們也說過了,它主要的作用就是封裝解析constructor-arg標籤得到的屬性,解析標籤對應的方法就是org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseConstructorArgElement,這個方法我就不帶大家看了,有興趣的可以自行閱讀。

它主要有兩個屬性

  1. indexedArgumentValues
  2. genericArgumentValues

對應的就是我們兩種指定參數的方法,如下:

<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" scope="prototype">
    <constructor-arg name="name" value="dmz"/>
    <constructor-arg name="age" value="18"/>
    <constructor-arg index="2"  value="2020-05-23"/>
    <!--		<constructor-arg name="birthDay" value="2020-05-23"/>-->
</bean>

其中的name跟age屬性會被解析為genericArgumentValues,而index=2會被解析為indexedArgumentValues

在對ConstructorArgumentValues有一定認知之後,我們再來看看resolveConstructorArguments的代碼:

// 方法目的:解析配置文件中指定的方法參數
// beanName:bean名稱
// mbd:beanName對應的beanDefinition
// bw:通過它進行類型轉換
// ConstructorArgumentValues cargs:解析標籤得到的屬性,還沒有經過解析(類型轉換)
// ConstructorArgumentValues resolvedValues:已經經過解析的參數
// 返回值:返回方法需要的最小參數個數
private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
                                        ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {
	
    // 是否有定製的類型轉換器,沒有的話直接使用BeanWrapper進行類型轉換
    TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
    TypeConverter converter = (customConverter != null ? customConverter : bw);
    
    // 構造一個BeanDefinitionValueResolver,專門用於解析constructor-arg中的value屬性,實際上還包括ref屬性,內嵌bean標籤等等
    BeanDefinitionValueResolver valueResolver =
        new BeanDefinitionValueResolver(this.beanFactory, beanName, mbd, converter);
	
    // minNrOfArgs 記錄執行方法要求的最小參數個數,一般情況下就是等於constructor-arg標籤指定的參數數量
    int minNrOfArgs = cargs.getArgumentCount();

    for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
        int index = entry.getKey();
        if (index < 0) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                            "Invalid constructor argument index: " + index);
        }
        // 這是啥意思呢?
        // 這個代碼我認為是有問題的,並且我給Spring官方已經提了一個issue,官方將會在5.2.7版本中修復
        // 暫且你先這樣理解
        // 假設A方法直接在配置文件中指定了index=3上要使用的參數,那麼這個時候A方法至少需要4個參數
        // 但是其餘的3個參數可能不是通過constructor-arg標籤指定的,而是直接自動注入進來的,那麼在配置文件中我們就只配置了index=3上的參數,也就是說 int minNrOfArgs = cargs.getArgumentCount()=1,這個時候 index=3,minNrOfArgs=1, 所以 minNrOfArgs = 3+1
        if (index > minNrOfArgs) {
            minNrOfArgs = index + 1;
        }
        ConstructorArgumentValues.ValueHolder valueHolder = entry.getValue();
        // 如果已經轉換過了,直接添加到resolvedValues集合中
        if (valueHolder.isConverted()) {
            resolvedValues.addIndexedArgumentValue(index, valueHolder);
        }
        else {
            // 解析value/ref/內嵌bean標籤等
            Object resolvedValue =
                valueResolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue());
            // 將解析后的resolvedValue封裝成一個新的ValueHolder,並將其source設置為解析constructor-arg得到的那個ValueHolder,後期會用到這個屬性進行判斷
            ConstructorArgumentValues.ValueHolder resolvedValueHolder =
                new ConstructorArgumentValues.ValueHolder(resolvedValue, valueHolder.getType(), valueHolder.getName());
            resolvedValueHolder.setSource(valueHolder);
            resolvedValues.addIndexedArgumentValue(index, resolvedValueHolder);
        }
    }
   // 對getGenericArgumentValues進行解析,代碼基本一樣,不再贅述
    return minNrOfArgs;
}

可以看到,最終的解析邏輯就在resolveValueIfNecessary這個方法中,那麼這個方法又做了什麼呢?

// 這個方法的目的就是將解析constructor-arg標籤得到的value值進行一次解析
// 在解析標籤時ref屬性會被封裝為RuntimeBeanReference,那麼在這裏進行解析時就會去調用getBean
// 在解析value屬性會會被封裝為TypedStringValue,那麼這裡會嘗試去進行一個轉換
// 關於標籤的解析大家有興趣的話可以去看看org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parsePropertyValue
// 這裏不再贅述了
public Object resolveValueIfNecessary(Object argName, @Nullable Object value) {
	
    // 解析constructor-arg標籤中的ref屬性,實際就是調用了getBean
    if (value instanceof RuntimeBeanReference) {
        RuntimeBeanReference ref = (RuntimeBeanReference) value;
        return resolveReference(argName, ref);
    }
    
    // ......
    
       /**  
		 * <constructor-arg>
		 * 			<set value-type="java.lang.String">
		 * 				<value>1</value>
		 * 			</set>
		 * </constructor-arg>
		 * 通過上面set標籤中的value-type屬性對value進行類型轉換,
		 * 如果value-type屬性為空,那麼這裏不會進行類型轉換
		 */
   else if (value instanceof TypedStringValue) {
			TypedStringValue typedStringValue = (TypedStringValue) value;
			Object valueObject = evaluate(typedStringValue);
			try {
				Class<?> resolvedTargetType = resolveTargetType(typedStringValue);
				if (resolvedTargetType != null) {
					return this.typeConverter.convertIfNecessary(valueObject, resolvedTargetType);
				}
				else {
					return valueObject;
				}
			}
			catch (Throwable ex) {
				// Improve the message by showing the context.
				throw new BeanCreationException(
						this.beanDefinition.getResourceDescription(), this.beanName,
						"Error converting typed String value for " + argName, ex);
			}
		}
    // 省略後續代碼....
}

就我們上面的例子而言,經過resolveValueIfNecessary方法並不能產生實際的影響,因為在XML中我們沒有配置ref屬性或者value-type屬性。

畫圖如下:

第五段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    //  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
    //  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    //	第三段代碼:以及從緩存中取過了對應了方法以及參數
    //  第四段代碼:明確了方法需要的最小的參數數量並對配置文件中的標籤屬性進行了一次解析

    // 進入第五段代碼分析
    
    // 保存在創建方法參數數組過程中發生的異常,如果最終沒有找到合適的方法,那麼將這個異常信息封裝后拋出
    LinkedList<UnsatisfiedDependencyException> causes = null;
    
    // 開始遍歷所有在第四段代碼中查詢到的符合要求的方法
    for (Method candidate : candidates) {
        // 方法的參數類型
        Class<?>[] paramTypes = candidate.getParameterTypes();
        // 候選的方法的參數必須要大於在第四段這推斷出來的最小參數個數
        if (paramTypes.length >= minNrOfArgs) {
            ArgumentsHolder argsHolder;
            // 必定為null,不考慮
            if (explicitArgs != null) {
                // Explicit arguments given -> arguments length must match exactly.
                if (paramTypes.length != explicitArgs.length) {
                    continue;
                }
                argsHolder = new ArgumentsHolder(explicitArgs);
            }
            else {
                // Resolved constructor arguments: type conversion and/or autowiring necessary.
                try {
                    // 獲取參數的具體名稱
                    String[] paramNames = null;
                    ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
                    if (pnd != null) {
                        paramNames = pnd.getParameterNames(candidate);
                    }
                    // 根據方法的參數名稱以及配置文件中配置的參數創建一個參數數組用於執行工廠方法
                    argsHolder = createArgumentArray(
                        beanName, mbd, resolvedValues, bw, paramTypes, paramNames, candidate, autowiring);
                }
                // 在創建參數數組的時候可能發生異常,這個時候的異常不能直接拋出,要確保所有的候選方法遍歷完成,只要有一個方法符合要求即可,但是如果遍歷完所有方法還是沒找到合適的構造器,那麼直接拋出這些異常
                catch (UnsatisfiedDependencyException ex) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Ignoring factory method [" + candidate + "] of bean '" + beanName + "': " + ex);
                    }
                    // Swallow and try next overloaded factory method.
                    if (causes == null) {
                        causes = new LinkedList<>();
                    }
                    causes.add(ex);
                    continue;
                }
                // 計算類型差異
                // 首先判斷bd中是寬鬆模式還是嚴格模式,目前看來只有@Bean標註的方法解析得到的Bean會使用嚴格模式來計算類型差異,其餘都是使用寬鬆模式
                // 嚴格模式下,
                int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
                                      argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
                // 選擇一個類型差異最小的方法
                if (typeDiffWeight < minTypeDiffWeight) {
                    factoryMethodToUse = candidate;
                    argsHolderToUse = argsHolder;
                    argsToUse = argsHolder.arguments;
                    minTypeDiffWeight = typeDiffWeight;
                    ambiguousFactoryMethods = null;
                }
   	// 省略後續代碼.......
    }

小總結:這段代碼的核心思想就是根據第四段代碼從配置文件中解析出來的參數構造方法執行所需要的實際參數數組。如果構建成功就代表這個方法可以用於實例化Bean,然後計算實際使用的參數跟方法上申明的參數的”差異值“,並在所有符合要求的方法中選擇一個差異值最小的方法

接下來,我們來分析方法實現的細節

  1. 構建方法使用的參數數組,也就是createArgumentArray方法,其源碼如下:
/* beanName:要實例化的Bean的名稱
 * mbd:對應Bean的BeanDefinition
 * resolvedValues:從配置文件中解析出來的並嘗試過類型轉換的參數
 * bw:在這裏主要就是用作類型轉換器
 * paramTypes:當前遍歷到的候選的方法的參數類型數組
 * paramNames:當前遍歷到的候選的方法的參數名稱
 * executable:當前遍歷到的候選的方法
 * autowiring:是否時自動注入
 */
private ArgumentsHolder createArgumentArray(
    String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,
			BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
			boolean autowiring) throws UnsatisfiedDependencyException {

		TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
		TypeConverter converter = (customConverter != null ? customConverter : bw);

		ArgumentsHolder args = new ArgumentsHolder(paramTypes.length);
		Set<ConstructorArgumentValues.ValueHolder> usedValueHolders = new HashSet<>(paramTypes.length);
		Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
		// 遍歷候選方法的參數,跟據方法實際需要的類型到resolvedValues中去匹配
		for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
			Class<?> paramType = paramTypes[paramIndex];
			String paramName = (paramNames != null ? paramNames[paramIndex] : "");
			
			ConstructorArgumentValues.ValueHolder valueHolder = null;
			if (resolvedValues != null) {
                // 首先,根據方法參數的下標到resolvedValues中找對應的下標的屬性
                // 如果沒找到再根據方法的參數名/類型去resolvedValues查找
				valueHolder = resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders);
				// 如果都沒找到
                // 1.是自動注入並且方法的參數長度正好跟配置中的參數數量相等
                // 2.不是自動注入
                // 那麼按照順序一次選取
				if (valueHolder == null && (!autowiring || paramTypes.length == resolvedValues.getArgumentCount())) {
					valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders);
				}
			}
			// 也就是說在配置的參數中找到了合適的值可以應用於這個方法上
			if (valueHolder != null) {
				// 防止同一個參數被應用了多次
				usedValueHolders.add(valueHolder);
				Object originalValue = valueHolder.getValue();
				Object convertedValue;
                // 已經進行過類型轉換就不會需要再次進行類型轉換
				if (valueHolder.isConverted()) {
					convertedValue = valueHolder.getConvertedValue();
					args.preparedArguments[paramIndex] = convertedValue;
				}
				else {
					// 嘗試將配置的值轉換成方法參數需要的類型
					MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
					try {
                        // 進行類型轉換
						convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);
					}
					catch (TypeMismatchException ex) {
						// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
					}
					Object sourceHolder = valueHolder.getSource();
                    // 只要是valueHolder存在,到這裏這個判斷必定成立
					if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) {
						Object sourceValue = ((ConstructorArgumentValues.ValueHolder) sourceHolder).getValue();
						args.resolveNecessary = true;
						args.preparedArguments[paramIndex] = sourceValue;
					}
				}

				args.arguments[paramIndex] = convertedValue;
				args.rawArguments[paramIndex] = originalValue;
			}
			else {
                // 方法執行需要參數,但是resolvedValues中沒有提供這個參數,也就是說這個參數是要自動注入到Bean中的
				MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
				// 不是自動注入,直接拋出異常
				if (!autowiring) {
					// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
				}
				try {
                    // 自動注入的情況下,調用getBean獲取需要注入的Bean
					Object autowiredArgument =
							resolveAutowiredArgument(methodParam, beanName, autowiredBeanNames, converter);
                    // 把getBean返回的Bean封裝到本次方法執行時需要的參數數組中去
					args.rawArguments[paramIndex] = autowiredArgument;
					args.arguments[paramIndex] = autowiredArgument;
                    // 標誌這個參數是自動注入的
					args.preparedArguments[paramIndex] = new AutowiredArgumentMarker();
                    // 自動注入的情況下,在第二次調用時,需要重新處理,不能直接緩存
					args.resolveNecessary = true;
				}
				catch (BeansException ex) {
					// 拋出UnsatisfiedDependencyException,在調用該方法處會被捕獲
				}
			}
		}
		
    	// 註冊Bean之間的依賴關係
		for (String autowiredBeanName : autowiredBeanNames) {
			this.beanFactory.registerDependentBean(autowiredBeanName, beanName);
			if (logger.isDebugEnabled()) {
				logger.debug("Autowiring by type from bean name '" + beanName +
						"' via " + (executable instanceof Constructor ? "constructor" : "factory method") +
						" to bean named '" + autowiredBeanName + "'");
			}
		}

		return args;
	}

上面這段代碼說難也難,說簡單也簡單,如果要徹底看懂它到底幹了什麼還是很有難度的。簡單來說,它就是從第四段代碼解析出來的參數中查找當前的這個候選方法需要的參數。如果找到了,那麼嘗試對其進行類型轉換,將其轉換成符合方法要求的類型,如果沒有找到那麼還需要判斷當前方法的這個參數能不能進行自動注入,如果可以自動注入的話,那麼調用getBean得到需要的Bean,並將其注入到方法需要的參數中。

第六段
public BeanWrapper instantiateUsingFactoryMethod(
    String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
    //  第一段代碼:到這裏已經得到了一個BeanWrapper,並對這個BeanWrapper做了初始化
    //  第二段代碼:明確了實例化當前這個Bean到底是靜態工廠還是實例工廠
    //	第三段代碼:以及從緩存中取過了對應了方法以及參數
    //  第四段代碼:明確了方法需要的最小的參數數量並對配置文件中的標籤屬性進行了一次解析
    //  第五段代碼:到這裏已經確定了可以使用來實例化Bean的方法是哪個
    
	// 省略拋出異常的代碼,就是在對推斷出來的方法做驗證
    // 1.推斷出來的方法不能為null
    // 2.推斷出來的方法返回值不能為void
    // 3.推斷出來的方法不能有多個
		
    // 對參數進行緩存
      if (explicitArgs == null && argsHolderToUse != null) {
         argsHolderToUse.storeCache(mbd, factoryMethodToUse);
      }
   }

   try {
      Object beanInstance;

      if (System.getSecurityManager() != null) {
         final Object fb = factoryBean;
         final Method factoryMethod = factoryMethodToUse;
         final Object[] args = argsToUse;
         beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () ->
               beanFactory.getInstantiationStrategy().instantiate(mbd, beanName, beanFactory, fb, factoryMethod, args),
               beanFactory.getAccessControlContext());
      }
      else {
          // 反射調用對應方法進行實例化
          // 1.獲取InstantiationStrategy,主要就是SimpleInstantiationStrategy跟CglibSubclassingInstantiationStrategy,其中CglibSubclassingInstantiationStrategy主要是用來處理beanDefinition中的lookupMethod跟replaceMethod。通常來說我們使用的就是SimpleInstantiationStrateg
          // 2.SimpleInstantiationStrateg就是單純的通過反射調用方法
         beanInstance = this.beanFactory.getInstantiationStrategy().instantiate(
               mbd, beanName, this.beanFactory, factoryBean, factoryMethodToUse, argsToUse);
      }
		// beanWrapper在這裏對Bean進行了包裝
      bw.setBeanInstance(beanInstance);
      return bw;
   }
   catch (Throwable ex) {
      throw new BeanCreationException(mbd.getResourceDescription(), beanName,
            "Bean instantiation via factory method failed", ex);
   }
}

上面這段代碼的主要目的就是

  1. 緩存參數,原型可能多次創建同一個對象
  2. 反射調用推斷出來的factoryMethod

》通過構造函數實例化對象

如果上面你對使用factoryMethd進行實例化對象已經足夠了解的話,那麼下面的源碼分析基本沒有什麼很大區別,我們接着看看代碼。

首先,我們回到createBeanInstance方法中,

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // 上面的代碼已經分析過了
    // 1.使用supplier來得到一個對象
    // 2.通過factotryMethod方法實例化一個對象
	
    // 看起來是不是有點熟悉,在使用factotryMethod創建對象時也有差不多這樣的一段代碼,看起來就是使用緩存好的方法直接創建一個對象
    boolean resolved = false;
    boolean autowireNecessary = false;
    
    // 不對這個參數進行討論,就認為一直為null
    if (args == null) {
        synchronized (mbd.constructorArgumentLock) {
            // bd中的resolvedConstructorOrFactoryMethod不為空,說明已經解析過構造方法了
            if (mbd.resolvedConstructorOrFactoryMethod != null) {
                // resolved標誌是否解析過構造方法
                resolved = true;
                autowireNecessary = mbd.constructorArgumentsResolved;
            }
        }
    }
    if (resolved) {
        // 構造函數已經解析過了,並且這個構造函數在調用時需要自動注入參數
        if (autowireNecessary) {
            // 此時部分解析好的參數已經存在了beanDefinition中,並且構造函數也在bd中
            // 那麼在這裏只會從緩存中去取構造函數以及參數然後反射調用
            return autowireConstructor(beanName, mbd, null, null);
        }
        else {
            // 這裏就是直接反射調用空參構造
            return instantiateBean(beanName, mbd);
        }
    }
	// 推斷出能夠使用的需要參數的構造函數
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    // 在推斷出來的構造函數中選取一個合適的方法來進行Bean的實例化
    // ctors不為null:說明存在1個或多個@Autowired標註的方法
    // mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR:說明是自動注入
    // mbd.hasConstructorArgumentValues():配置文件中配置了構造函數要使用的參數
    // !ObjectUtils.isEmpty(args):外部傳入的參數,必定為null,不多考慮
    // 上面的條件只要滿足一個就會進入到autowireConstructor方法
    // 第一個條件滿足,那麼通過autowireConstructor在推斷出來的構造函數中再進一步選擇一個差異值最小的,參數最長的構造函數
    // 第二個條件滿足,說明沒有@Autowired標註的方法,但是需要進行自動注入,那麼通過autowireConstructor會去遍歷類中申明的所有構造函數,並查找一個差異值最小的,參數最長的構造函數
    // 第三個條件滿足,說明不是自動注入,那麼要通過配置中的參數去類中申明的所有構造函數中匹配
    // 第四個必定為null,不考慮
    if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
        return autowireConstructor(beanName, mbd, ctors, args);
    }
	
    // 反射調用空參構造
    return instantiateBean(beanName, mbd);
}

因為autowireConstructor方法的執行邏輯跟instantiateUsingFactoryMethod方法的執行邏輯基本一致,只是將Method對象換成了Constructor對象,所以對這個方法我不再做詳細的分析。

我們主要就看看determineConstructorsFromBeanPostProcessors這個方法吧,這個方法的主要目的就是推斷出候選的構造方法。

determineConstructorsFromBeanPostProcessors方法做了什麼?

// 實際調用的就是AutowiredAnnotationBeanPostProcessor中的determineCandidateConstructors方法
// 這個方法看起來很長,但實際確很簡單,就是通過@Autowired註解確定哪些構造方法可以作為候選方法,其實在使用factoryMethod來實例化對象的時候也有這種邏輯在其中,後續在總結的時候我們對比一下
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
			throws BeanCreationException {

		// 這裏做的事情很簡單,就是將@Lookup註解標註的方法封裝成LookupOverride添加到BeanDefinition中的methodOverrides屬性中,如果這個屬性不為空,在實例化對象的時候不能選用SimpleInstantiationStrateg,而要使用CglibSubclassingInstantiationStrategy,通過cglib代理給方法加一層攔截了邏輯
    	// 避免重複檢查
		if (!this.lookupMethodsChecked.contains(beanName)) {
			try {
				ReflectionUtils.doWithMethods(beanClass, method -> {
					Lookup lookup = method.getAnnotation(Lookup.class);
					if (lookup != null) {
						Assert.state(this.beanFactory != null, "No BeanFactory available");				// 將@Lookup註解標註的方法封裝成LookupOverride
						LookupOverride override = new LookupOverride(method, lookup.value());
						try {
                            // 添加到BeanDefinition中的methodOverrides屬性中
							RootBeanDefinition mbd = (RootBeanDefinition)
									this.beanFactory.getMergedBeanDefinition(beanName);
							mbd.getMethodOverrides().addOverride(override);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(beanName,
									"Cannot apply @Lookup to beans without corresponding bean definition");
						}
					}
				});
			}
			catch (IllegalStateException ex) {
				throw new BeanCreationException(beanName, "Lookup method resolution failed", ex);
			}
			this.lookupMethodsChecked.add(beanName);
		}
		
    // 接下來要開始確定到底哪些構造函數能被作為候選者
		
    // 先嘗試從緩存中獲取
		Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
		if (candidateConstructors == null) {
			// Fully synchronized resolution now...
			synchronized (this.candidateConstructorsCache) {
				candidateConstructors = this.candidateConstructorsCache.get(beanClass);、
                    // 緩存中無法獲取到,進入正式的推斷過程
				if (candidateConstructors == null) {
					Constructor<?>[] rawCandidates;
					try {
                        // 第一步:先查詢這個類所有的構造函數,包括私有的
						rawCandidates = beanClass.getDeclaredConstructors();
					}
					catch (Throwable ex) {
						// 省略異常信息
					}
					List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                    // 保存添加了Autowired註解並且required屬性為true的構造方法
					Constructor<?> requiredConstructor = null;
                    // 空參構造
					Constructor<?> defaultConstructor = null;
                    // 看方法註釋上說明的,這裏除非是kotlin的類,否則必定為null,不做過多考慮,我們就將其當作null
					Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);
					int nonSyntheticConstructors = 0;
                    // 對類中的所有構造方法進行遍歷
					for (Constructor<?> candidate : rawCandidates) {
                        // 非合成方法
						if (!candidate.isSynthetic()) {
							nonSyntheticConstructors++;
						}
						else if (primaryConstructor != null) {
							continue;
						}
                        // 查詢方法上是否有Autowired註解
						AnnotationAttributes ann = findAutowiredAnnotation(candidate);
						if (ann == null) {
                            // userClass != beanClass說明這個類進行了cglib代理
							Class<?> userClass = ClassUtils.getUserClass(beanClass);
							if (userClass != beanClass) {
								try {
                                    // 如果進行了cglib代理,那麼在父類上再次查找Autowired註解
									Constructor<?> superCtor =
											userClass.getDeclaredConstructor(candidate.getParameterTypes());
									ann = findAutowiredAnnotation(superCtor);
								}
								catch (NoSuchMethodException ex) {
									// Simply proceed, no equivalent superclass constructor found...
								}
							}
						}
                        // 說明當前的這個構造函數上有Autowired註解
						if (ann != null) {
							if (requiredConstructor != null) {
								// 省略異常拋出
							}
                            // 獲取Autowired註解中的required屬性
							boolean required = determineRequiredStatus(ann);
							if (required) {
                                // 類中存在多個@Autowired標註的方法,並且某個方法的@Autowired註解上被申明了required屬性要為true,那麼直接報錯
								if (!candidates.isEmpty()) {
								// 省略異常拋出
								}
								requiredConstructor = candidate;
							}
                            // 添加到集合中,這個集合存儲的都是被@Autowired註解標註的方法
							candidates.add(candidate);
						}
                        // 空參構造函數
						else if (candidate.getParameterCount() == 0) {
							defaultConstructor = candidate;
						}
					}
					if (!candidates.isEmpty()) {
						// 存在多個被@Autowired標註的方法
                        // 並且所有的required屬性被設置成了false (默認為true)
						if (requiredConstructor == null) {
                            // 存在空參構造函數,注意,空參構造函數可以不被@Autowired註解標註
							if (defaultConstructor != null) {
                                // 將空參構造函數也加入到候選的方法中去
								candidates.add(defaultConstructor);
							}
							// 省略日誌打印
						}
                        
						candidateConstructors = candidates.toArray(new Constructor<?>[0]);
					}
                    // 也就是說,類中只提供了一個構造函數,並且這個構造函數不是空參構造函數
					else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
						candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
					}
                    // 省略中間兩個判斷,primaryConstructor必定為null,不考慮
					// .....
					}
					else {
                        // 說明無法推斷出來
						candidateConstructors = new Constructor<?>[0];
					}
					this.candidateConstructorsCache.put(beanClass, candidateConstructors);
				}
			}
		}
		return (candidateConstructors.length > 0 ? candidateConstructors : null);
	}

這裏我簡單總結下這個方法的作用

  1. 獲取到類中的所有構造函數

  2. 查找到被@Autowired註解標註的構造函數

    • 如果存在多個被@Autowired標註的構造函數,並且其required屬性沒有被設置為true,那麼返回這些被標註的函數的集合(空參構造即使沒有添加@Autowired也會被添加到集合中)
    • 如果存在多個被@Autowired標註的構造函數,並且其中一個的required屬性被設置成了true,那麼直接報錯
    • 如果只有一個構造函數被@Autowired標註,並且其required屬性被設置成了true,那麼直接返回這個構造函數
  3. 如果沒有被@Autowired標註標註的構造函數,但是類中有且只有一個構造函數,並且這個構造函數不是空參構造函數,那麼返回這個構造函數

  4. 上面的條件都不滿足,那麼determineCandidateConstructors這個方法就無法推斷出合適的構造函數了

可以看到,通過AutowiredAnnotationBeanPostProcessordetermineCandidateConstructors方法可以處理構造函數上的@Autowired註解。

但是,請注意,這個方法並不能決定到底使用哪個構造函數來創建對象(即使它只推斷出來一個,也不一定能夠使用),它只是通過@Autowired註解來確定構造函數的候選者,在構造函數都沒有添加@Autowired註解的情況下,這個方法推斷不出來任何方法。真正確定到底使用哪個構造函數是交由autowireConstructor方法來決定的。前文已經分析過了instantiateUsingFactoryMethod方法,autowireConstructor的邏輯基本跟它一致,所以這裏不再做詳細的分析。

factoryMethod跟構造函數的比較

整體邏輯比較

從上圖中可以看到,整體邏輯上它們並沒有什麼區別,只是查找的對象從factoryMethod換成了構造函數

執行細節比較

細節的差異主要體現在推斷方法上

  • 推斷factoryMethod
  • 推斷構造函數

它們之間的差異我已經在圖中標識出來了,主要就是兩點

  1. 通過構造函數實例化對象,多了一層處理,就是要處理構造函數上的@Autowired註解以及方法上的@LookUp註解(要決定選取哪一種實例化策略,SimpleInstantiationStrategy/CglibSubclassingInstantiationStrategy
  2. 在最終的選取也存在差異,對於facotyMehod而言,在寬鬆模式下(除ConfigurationClassBeanDefinition外,也就是掃描@Bean得到的BeanDefinition,都是寬鬆模式),會選取一個最精準的方法,在嚴格模式下,會選取一個參數最長的方法
  3. 對於構造函數而言,會必定會選取一個參數最長的方法

關於計算類型差異的補充內容

思考了很久,我還是決定再補充一些內容,就是關於上面兩幅圖的最後一步,對應的核心代碼如下:

int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
                      argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));

if (typeDiffWeight < minTypeDiffWeight) {
    factoryMethodToUse = candidate;
    argsHolderToUse = argsHolder;
    argsToUse = argsHolder.arguments;
    minTypeDiffWeight = typeDiffWeight;
    ambiguousFactoryMethods = null;
}
  1. 判斷bd是嚴格模式還是寬鬆模式,上面說過很多次了,bd默認就是寬鬆模式,只要在ConfigurationClassBeanDefinition中使用嚴格模式,也就是掃描@Bean標註的方法註冊的bd(對應的代碼可以參考:org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod方法)

    我們再看看嚴格模式跟寬鬆模式在計算差異值時的區別

    • 寬鬆模式
    public int getTypeDifferenceWeight(Class<?>[] paramTypes) {
        // 計算實際使用的參數跟方法申明的參數的差異值
    			int typeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.arguments);
        // 計算沒有經過類型轉換的參數跟方法申明的參數的差異值
    			int rawTypeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.rawArguments) - 1024;
    			return (rawTypeDiffWeight < typeDiffWeight ? rawTypeDiffWeight : typeDiffWeight);
    		}
    
    public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) {
        int result = 0;
        for (int i = 0; i < paramTypes.length; i++) {
            // 在出現類型轉換時,下面這個判斷才會成立,也就是在比較rawArguments跟paramTypes的差異時才可能滿足這個條件
            if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) {
                return Integer.MAX_VALUE;
            }
            if (args[i] != null) {
                Class<?> paramType = paramTypes[i];
                Class<?> superClass = args[i].getClass().getSuperclass();
                while (superClass != null) {
                    // 如果我們傳入的值是方法上申明的參數的子類,那麼每多一層繼承關係,差異值加2
                    if (paramType.equals(superClass)) {
                        result = result + 2;
                        superClass = null;
                    }
                    else if (ClassUtils.isAssignable(paramType, superClass)) {
                        result = result + 2;
                        superClass = superClass.getSuperclass();
                    }
                    else {
                        superClass = null;
                    }
                }
                // 判斷方法的參數是不是一個接口,如果是,那麼差異值加1
                if (paramType.isInterface()) {
                    result = result + 1;
                }
            }
        }
        return result;
    }
    
    • 嚴格模式(主要應用於@Bean標註的方法對應的BeanDefinition)
    public int getAssignabilityWeight(Class<?>[] paramTypes) {
        // 嚴格模式下,只有三種返回值
        // 1.Integer.MAX_VALUE,經過類型轉換后還是不符合要求,返回最大的類型差異
        // 因為解析后的參數可能返回一個NullBean(創建對象的方法返回了null,Spring會將其包裝成一個NullBean),不過一般不會出現這種情況,所以我們可以當這種情況不存在
    			for (int i = 0; i < paramTypes.length; i++) {
    				if (!ClassUtils.isAssignableValue(paramTypes[i], this.arguments[i])) {
    					return Integer.MAX_VALUE;
    				}
    			}
        // 2.Integer.MAX_VALUE - 512,進行過了類型轉換才符合要求
    			for (int i = 0; i < paramTypes.length; i++) {
    				if (!ClassUtils.isAssignableValue(paramTypes[i], this.rawArguments[i])) {
    					return Integer.MAX_VALUE - 512;
    				}
    			}
        // 3.Integer.MAX_VALUE - 1024,沒有經過類型轉換就已經符合要求了,返回最小的類型差異
    			return Integer.MAX_VALUE - 1024;
    		}
    

    首先,不管是factoryMethod還是constructor,都是採用上面的兩個方法來計算類型差異,但是正常來說,只有factoryMethod會採用到嚴格模式(除非程序員手動干預,比如通過Bean工廠後置處理器修改了bd中的屬性,這樣通常來說沒有很大意義)

    所以我們分為三種情況討論

    1、factoryMethod+寬鬆模式

    這種情況下,會選取一個最精確的方法,同時方法的參數要盡量長

    測試代碼:

    public class FactoryObject {
    
    	public DmzService getDmz() {
    		System.out.println(0);
    		return new DmzService();
    	}
    
    	public DmzService getDmz(OrderService indexService) {
    		System.out.println(1);
    		return new DmzService();
    	}
    
    	public DmzService getDmz(OrderService orderService, IndexService indexService) {
    		System.out.println(2);
    		return new DmzService();
    	}
    
        public DmzService getDmz(OrderService orderService, IndexService indexService,IA ia) {
            System.out.println(3);
            return new DmzService();
        }
    }
    
    public class ServiceImpl implements IService {
    }
    
    public class IAImpl implements IA {
    }
    
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-autowire="constructor"><!--必須要開啟自動注入,並且是通過構造函數進行自動注入,否則選用無參構造-->
    
    	<!--factoryObject 提供了創建對象的方法-->
    	<bean id="factoryObject" class="com.dmz.spring.instantiation.service.FactoryObject"/>
    
    	<bean class="com.dmz.spring.instantiation.service.OrderService" id="orderService"/>
    
    	<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz" />
    
    	<bean class="com.dmz.spring.instantiation.service.ServiceImpl" id="iService"/>
    
    	<bean class="com.dmz.spring.instantiation.service.IndexService" id="indexService"/>
    
    </beans>
    
    /**
     * @author 程序員DMZ
     * @Date Create in 23:59 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class XMLMain {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext cc =
    				new ClassPathXmlApplicationContext("application.xml");
    	}
    }
    

    運行程序發現,選用了第三個(getDmz(OrderService orderService, IndexService indexService))構造方法。雖然最後一個方法的參數更長,但是因為其方法申明的參數上存在接口,所以它的差異值會大於第三個方法,因為不會被選用

    2、factoryMethod+嚴格模式

    這種情況下,會選取一個參數盡量長的方法

    測試代碼:

    /**
     * @author 程序員DMZ
     * @Date Create in 6:28 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    @ComponentScan("com.dmz.spring.instantiation")
    @Configuration
    public class Config {
    
    	@Bean
    	public DmzService dmzService() {
    		System.out.println(0);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService indexService) {
    		System.out.println(1);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService) {
    		System.out.println(2);
    		return new DmzService();
    	}
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService, IA ia) {
    		System.out.println("config " +3);
    		return new DmzService();
    	}
    
    	@Bean
    	public DmzService dmzService(OrderService orderService, IndexService indexService, IA ia, IService iService) {
    		System.out.println("config " +4);
    		return new DmzService();
    	}
    }
    
    /**
     * @author 程序員DMZ
     * @Date Create in 6:29 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class Main {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    		ac.register(Config.class);
    		ac.refresh();
    	}
    }
    

    運行程序,發現選用了最後一個構造函數,這是因為在遍歷候選方法時,會先遍歷參數最長的,而在計算類型差異時,因為嚴格模式下,上面所有方法的差異值都是一樣的,都會返回Integer.MAX_VALUE - 1024。實際上,在不進行手動干預的情況下,都會返滬這個值。

    3、構造函數+寬鬆模式

    這種情況下,也會選取一個參數盡量長的方法

    之所以會這樣,主要是因為在autowireConstructor方法中進行了一次短路判斷,如下所示:

    在上圖中,如果已經找到了合適的方法,那麼直接就不會再找了,而在遍歷的時候是從參數最長的方法開始遍歷的,測試代碼如下:

    @Component
    public class DmzService {
    	
        // 沒有添加@Autowired註解,也會被當作候選方法
    	public DmzService(){
    		System.out.println(0);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService) {
    		System.out.println(1);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService, IService iService) {
    		System.out.println(2);
    	}
    
    	@Autowired(required = false)
    	public DmzService(OrderService orderService, IndexService indexService, IService iService,IA ia) {
    		System.out.println("DmzService "+3);
    	}
    }
    
    /**
     * @author 程序員DMZ
     * @Date Create in 6:29 2020/6/1
     * @Blog https://daimingzhi.blog.csdn.net/
     */
    public class Main {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    		ac.register(Config.class);
    		ac.refresh();
    	}
    }
    

    這篇文章就到這裏啦~~!

    文章很長,希望你耐心看完,碼字不易,如果有幫助到你的話點個贊吧~!

掃描下方二維碼,關注我的公眾號,更多精彩文章在等您!~~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

Flutter實戰】文本組件及五大案例

老孟導讀:大家好,這是【Flutter實戰】系列文章的第二篇,這一篇講解文本組件,文本組件包括文本展示組件(Text和RichText)和文本輸入組件(TextField),基礎用法和五個案例助你快速掌握。
第一篇鏈接:【Flutter實戰】移動技術發展史

Text

Text是显示文本的組件,最常用的組件,沒有之一。基本用法如下:

Text('老孟')

注意:Text組件一定要包裹在Scaffold組件下,否則效果如下:

文本的樣式在style中設置,類型為TextStyleTextStyle中包含很多文本樣式屬性,下面介紹一些常用的。

設置文本大小和顏色:

Text('老孟',style: TextStyle(color: Colors.red,fontSize: 20),),

上面黑色的字體為沒有設置的效果,作為對比。

設置字體粗細:

Text('老孟',style: TextStyle(fontWeight: FontWeight.bold))

字體粗細共有9個級別,為w100w900FontWeight.boldw700

設置斜體:

Text('老孟',style: TextStyle(fontStyle: FontStyle.italic,))

設置自定義的字體:

  1. 首先下載字體庫(比如中華字體庫)
  2. 將字體文件拷貝的項目中,一般目錄是:assets/fonts/,assets和fonts都需要手動創建,此目錄不是必須的,而是約定俗成,資源一般都放在assets目錄下。
  3. 配置pubspec.yaml
fonts:
  - family: maobi 
    fonts:
      - asset: assets/fonts/maobi.ttf

maobi:是自己對當前字體的命名,有意義即可。

asset:字體文件的目錄。

使用:

Text('老孟', style: TextStyle(fontFamily: 'maobi',)),

設置對齊方式:

Container(
  height: 100,
  width: 200,
  color: Colors.blue.withOpacity(.4),
  child: Text('老孟', textAlign: TextAlign.center),
),

textAlign只是控制水平方向的對齊方式,值說明如下:

  • left:左對齊
  • right:右對齊
  • center:居中
  • justify:兩端對齊,此屬性中文存在bug(Flutter版本:1.17.3)也可以在官方issue中關注此問題
  • start:前端對齊,和TextDirection屬性有關,如果設置TextDirection.ltr,則左對齊,設置TextDirection.rtl則右對齊。
  • end:末端對齊,和TextDirection屬性有關,如果設置TextDirection.ltr,則右對齊,設置TextDirection.rtl則左對齊。

設置文本自動換行:

Container(
  height: 100,
  width: 200,
  color: Colors.blue.withOpacity(.4),
  child: Text('老孟,專註分享Flutter技術和應用實戰',softWrap: true,),
)

文本超出範圍時的處理:

Container(
  height: 100,
  width: 200,
  color: Colors.blue.withOpacity(.4),
  child: Text('老孟,專註分享Flutter技術和應用實戰',overflow: TextOverflow.ellipsis,),
)

溢出的處理方式:

  • clip:直接裁剪。
  • fade:越來越透明。
  • ellipsis:省略號結尾。
  • visible:依然显示,此時將會溢出父組件。

設置全局字體樣式:

MaterialApptheme中設置如下

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
   ...
    textTheme: TextTheme(
        bodyText2: TextStyle(color: Colors.red,fontSize: 24),
    )
  ),
  home: Scaffold(
    body: TextDemo(),
  ),
)

Text組件默認為紅色,

Text('老孟'),

Text('老孟',style: TextStyle(color: Colors.blue,fontSize: 20),),

RichText

RichText的屬性和Text基本一樣,使用如下:

RichText(
  text: TextSpan(
      style: DefaultTextStyle.of(context).style,
      children: <InlineSpan>[
        TextSpan(text: '老孟', style: TextStyle(color: Colors.red)),
        TextSpan(text: ','),
        TextSpan(text: '專註分享Flutter技術和應用實戰'),
      ]),
)

TextField

TextField是文本輸入組件,即輸入框,常用組件之一。基本用法:

TextField()

不需要任何參數,一個最簡單的文本輸入組件就出來了,效果如下:

decoration是TextField組件的裝飾(外觀)參數,類型是InputDecoration。

icon显示在輸入框的前面,用法如下:

TextField(
  decoration: InputDecoration(
    icon: Icon(Icons.person),
  ),
)

當輸入框是空而且沒有焦點時,labelText显示在輸入框上邊,當獲取焦點或者不為空時labelText往上移動一點,labelStyle參數表示文本樣式,具體參考TextStyle, 用法如下:

TextField(
  decoration: InputDecoration(
    labelText: '姓名:',
    labelStyle: TextStyle(color:Colors.red)
  ),
)

hasFloatingPlaceholder參數控制當輸入框獲取焦點或者不為空時是否還显示labelText,默認為true,显示。

helperText显示在輸入框的左下部,用於提示用戶,helperStyle參數表示文本樣式,具體參考TextStyle用法如下:

TextField(
  decoration: InputDecoration(
    helperText: '用戶名長度為6-10個字母',
    helperStyle: TextStyle(color: Colors.blue),
    helperMaxLines: 1
  ),
)

hintText是當輸入框為空時的提示,不為空時不在显示,用法如下:

TextField(
  decoration: InputDecoration(
    hintText: '請輸入用戶名',
    hintStyle: TextStyle(color: Colors.grey),
    hintMaxLines: 1
  ),
)

errorText显示在輸入框的左下部,默認字體為紅色,用法如下:

TextField(
  decoration: InputDecoration(
    errorText: '用戶名輸入錯誤',
    errorStyle: TextStyle(fontSize: 12),
    errorMaxLines: 1,
    errorBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.red)),
  ),
)

prefix系列的組件是輸入框前面的部分,用法如下:

TextField(
  decoration: InputDecoration(
    prefixIcon: Icon(Icons.person)
  ),
)

注意prefix和icon的區別,icon是在輸入框邊框的外部,而prefix在裏面。

suffix和prefix相反,suffix在輸入框的尾部,用法如下:

TextField(
  decoration: InputDecoration(
      suffixIcon: Icon(Icons.person)
  ),
)

counter組件統計輸入框文字的個數,counter僅僅是展示效果,不具備自動統計字數的功能, 自動統計字數代碼如下:

var _textFieldValue = '';
TextField(
  onChanged: (value){
    setState(() {
      _textFieldValue = value;
    });
  },
  decoration: InputDecoration(
    counterText: '${_textFieldValue.length}/32'
  ),
)

filled為true時,輸入框將會被fillColor填充,仿QQ登錄輸入框代碼如下:

Container(
  height: 60,
  width: 250,
  child: TextField(
    decoration: InputDecoration(
      fillColor: Color(0x30cccccc),
      filled: true,
      enabledBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Color(0x00FF0000)),
          borderRadius: BorderRadius.all(Radius.circular(100))),
      hintText: 'QQ號/手機號/郵箱',
      focusedBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Color(0x00000000)),
          borderRadius: BorderRadius.all(Radius.circular(100))),
    ),
  ),
)

controller是輸入框文本編輯的控制器,可以獲取TextField的內容、設置TextField的內容,下面將輸入的英文變為大寫:

TextEditingController _controller;

@override
void initState() {
  super.initState();
  _controller = TextEditingController()
    ..addListener(() {
      //獲取輸入框的內容,變為大寫
      _controller.text = _controller.text.toUpperCase();

    });
}

@override
Widget build(BuildContext context) {
  return TextField(
    controller: _controller,
  );
}

@override
dispose() {
  super.dispose();
  _controller.dispose();
}

有時輸入框後面帶有“清除”功能,需要controller來實現。如果需要2個TextField的內容進行同步,只需要給2個TextField設置同一個controller即可實現。

keyboardType參數控制軟鍵盤的類型,說明如下:

  • text:通用鍵盤。
  • multiline:當TextField為多行時(maxLines設置大於1),右下角的為“換行” 按鍵。
  • number:数字鍵盤。
  • phone:手機鍵盤,比数字鍵盤多”*”和 “#”。
  • datetime:在ios上和text一樣,在android上出現数字鍵盤、”:”和 “-“。
  • emailAddress:郵箱鍵盤,有”@” 和 “.”按鍵。
  • url:url鍵盤,有”/” 和 “.”按鍵。
  • visiblePassword:既有字幕又有数字的鍵盤。

textInputAction參數控制軟鍵盤右下角的按鍵,說明如下:

  • none:android上显示返回鍵,ios不支持。
  • unspecified:讓操作系統自己決定哪個合適,一般情況下,android显示“完成”或者“返回”。
  • done:android显示代表“完成”的按鈕,ios显示“Done”(中文:完成)。
  • go:android顯示錶達用戶去向目的地的圖標,比如向右的箭頭,ios显示“Go”(中文:前往)。
  • search:android顯示錶達搜索的按鈕,ios显示”Search”(中文:搜索)。
  • send:android顯示錶達發送意思的按鈕,比如“紙飛機”按鈕,ios显示”Send”(中文:發送)。
  • next:android顯示錶達“前進”的按鈕,比如“向右的箭頭”,ios显示”Next”(中文:下一項)。
  • previous:android顯示錶達“後退”的按鈕,比如“向左的箭頭”,ios不支持。
  • continueAction:android 不支持,ios僅在ios9.0+显示”Continue”(中文:繼續)。
  • join:Android和ios显示”Join”(中文:加入)。
  • route:android 不支持,ios显示”Route”(中文:路線)。
  • emergencyCall:android 不支持,ios显示”Emergency Call”(中文:緊急電話)。
  • newline:android顯示錶達“換行”的按鈕,ios显示”換行“。

大家可能發現了,Android上显示的按鈕大部分是不確定的,比如next有的显示向右的箭頭,有的显示前進,這是因為各大廠商對Android ROM定製引發的。

textCapitalization參數是配置鍵盤是大寫還是小寫,僅支持鍵盤模式為text,其他模式下忽略此配置,說明如下:

  • words:每一個單詞的首字母大寫。
  • sentences:每一句話的首字母大寫。
  • characters:每個字母都大寫
  • none:都小寫

這裏僅僅是控制軟鍵盤是大寫模式還是小寫模式,你也可以切換大小寫,系統並不會改變輸入框內的內容。

textAlignVertical表示垂直方向的對齊方式,textDirection表示文本方向,用法如下:

TextField(
  textAlignVertical: TextAlignVertical.center,
  textDirection: TextDirection.rtl,
)

toolbarOptions表示長按時彈出的菜單,有copycutpasteselectAll,用法如下:

TextField(
  toolbarOptions: ToolbarOptions(
    copy: true,
    cut: true,
    paste: true,
    selectAll: true
  ),
)

cursor表示光標,用法如下:

TextField(
  showCursor: true,
  cursorWidth: 3,
  cursorRadius: Radius.circular(10),
  cursorColor: Colors.red,
)

效果如下:

將輸入框設置為密碼框,只需obscureText屬性設置true即可,用法如下:

TextField(
  obscureText: true,
)

通過inputFormatters可以限制用戶輸入的內容,比如只想讓用戶輸入字符,設置如下:

TextField(
  inputFormatters: [
    WhitelistingTextInputFormatter(RegExp("[a-zA-Z]")),
  ],
)

這時用戶是無法輸入数字的。

onChanged是當內容發生變化時回調,onSubmitted是點擊回車或者點擊軟鍵盤上的完成回調,onTap點擊輸入框時回調,用法如下:

TextField(
  onChanged: (value){
    print('onChanged:$value');
  },
  onEditingComplete: (){
    print('onEditingComplete');
  },
  
  onTap: (){
    print('onTap');
  },
)

輸入框右下角經常需要字數統計,除了使用上面介紹的方法外,還可以使用buildCounter,建議使用此方法,用法如下:

TextField(
  maxLength: 100,
  buildCounter: (
    BuildContext context, {
    int currentLength,
    int maxLength,
    bool isFocused,
  }) {
    return Text(
      '$currentLength/$maxLength',
    );
  },
)

動態獲取焦點

FocusScope.of(context).requestFocus(_focusNode);

_focusNode為TextField的focusNode:

_focusNode = FocusNode();

TextField(
	focusNode: _focusNode,
	...
)

動態失去焦點

_focusNode.unfocus();

過渡顏色的文字

Builder(
  builder: (BuildContext context) {
    RenderBox box = context.findRenderObject();
    final Shader linearGradient = LinearGradient(
      colors: <Color>[Colors.purple, Colors.blue],
    ).createShader(
        Rect.fromLTWH(0.0, 0.0, box?.size?.width, box?.size?.height));

    return Text(
      '老孟,專註分享Flutter技術和應用實戰',
      style: new TextStyle(
          fontSize: 18.0,
          fontWeight: FontWeight.bold,
          foreground: Paint()..shader = linearGradient),
    );
  },
)

Builder組件是為了計算當前Text組件大小,生成LinearGradient。

帶前後置標籤的文本

RichText(
  text: TextSpan(
      style: DefaultTextStyle.of(context).style,
      children: <InlineSpan>[
        WidgetSpan(
            child: Container(
              margin: EdgeInsets.only(right: 10),
              padding: EdgeInsets.symmetric(horizontal: 10),
              decoration: BoxDecoration(
                  shape: BoxShape.rectangle,
                  borderRadius: BorderRadius.all(Radius.circular(20)),
                  color: Colors.blue),
              child: Text(
                '判斷題',
                style: TextStyle(color: Colors.white),
              ),
            )),
        TextSpan(text: '泡沫滅火器可用於帶電滅火'),

      ]),
)

“服務協議”

通常在登錄頁面的底部會出現登錄即代表同意並閱讀 《服務協議》,其中《服務協議》為藍色且可點擊:

Text.rich(
  TextSpan(
      text: '登錄即代表同意並閱讀',
      style: TextStyle(fontSize: 11, color: Color(0xFF999999)),
      children: [
        TextSpan(
          text: '《服務協議》',
          style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
          recognizer: TapGestureRecognizer()
            ..onTap = () {
              print('onTap');
            },
        ),
      ]),
)

登錄密碼輸入框

TextField(
  decoration: InputDecoration(
    fillColor: Color(0x30cccccc),
    filled: true,
    enabledBorder: OutlineInputBorder(
        borderSide: BorderSide(color: Color(0x00FF0000)),
        borderRadius: BorderRadius.all(Radius.circular(100))),
    hintText: '輸入密碼',
    focusedBorder: OutlineInputBorder(
        borderSide: BorderSide(color: Color(0x00000000)),
        borderRadius: BorderRadius.all(Radius.circular(100))),

  ),
  textAlign: TextAlign.center,
  obscureText: true,
  onChanged: (value) {

  },
)

評論回復

Text.rich(
  TextSpan(
      text: '回復',
      style: TextStyle(fontSize: 11, color: Color(0xFF999999)),
      children: [
        TextSpan(
          text: '@老孟:',
          style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
          recognizer: TapGestureRecognizer()
            ..onTap = () {
              print('onTap');
            },
        ),
        TextSpan(
          text: '你好,想知道Flutter發展前景如何?',
        ),
      ]),
)

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

數據源管理 | Kafka集群環境搭建,消息存儲機制詳解

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、Kafka集群環境

1、環境版本

版本:kafka2.11,zookeeper3.4

注意:這裏zookeeper3.4也是基於集群模式部署。

2、解壓重命名

tar -zxvf kafka_2.11-0.11.0.0.tgz
mv kafka_2.11-0.11.0.0 kafka2.11

創建日誌目錄

[root@en-master kafka2.11]# mkdir logs

注意:以上操作需要同步到集群下其他服務上。

3、添加環境變量

vim /etc/profile
export KAFKA_HOME=/opt/kafka2.11
export PATH=$PATH:$KAFKA_HOME/bin
source /etc/profile

4、修改核心配置

[root@en-master /opt/kafka2.11/config]# vim server.properties
-- 核心修改如下
# 唯一編號
broker.id=0
# 開啟topic刪除
delete.topic.enable=true
# 日誌地址
log.dirs=/opt/kafka2.11/logs
# zk集群
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181

注意:broker.id安裝集群服務個數編排即可,集群下不能重複。

5、啟動kafka集群

# 啟動命令
[root@node02 kafka2.11]# bin/kafka-server-start.sh -daemon config/server.properties
# 停止命令
[root@node02 kafka2.11]# bin/kafka-server-stop.sh
# 進程查看
[root@node02 kafka2.11]# jps

注意:這裏默認啟動了zookeeper集群服務,並且集群下的kafka分別啟動。

6、基礎管理命令

創建topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--create --replication-factor 3 --partitions 1 --topic one-topic

參數說明:

  • replication-factor 定義副本個數
  • partitions 定義分區個數
  • topic:定義topic名稱

查看topic列表

bin/kafka-topics.sh --zookeeper zk01:2181 --list

修改topic分區

bin/kafka-topics.sh --zookeeper zk01:2181 --alter --topic one-topic --partitions 5

查看topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--describe --topic one-topic

發送消息

bin/kafka-console-producer.sh \
--broker-list 192.168.72.133:9092 --topic one-topic

消費消息

bin/kafka-console-consumer.sh \
--bootstrap-server 192.168.72.133:9092 --from-beginning --topic one-topic

刪除topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--delete --topic first

7、Zk集群用處

Kafka集群中有一個broker會被選舉為Controller,Controller依賴Zookeeper環境,管理集群broker的上下線,所有topic的分區副本分配和leader選舉等工作。

二、消息攔截案例

1、攔截器簡介

Kafka中間件的Producer攔截器主要用於實現消息發送的自定義控制邏輯。用戶可以在消息發送前以及回調邏輯執行前有機會對消息做一些自定義,比如消息修改等,發送狀態監控等,用戶可以指定多個攔截器按順序執行攔截。

核心方法

  • configure:獲取配置信息和初始化數據時調用;
  • onSend:消息被序列化以及和計算分區前調用該方法,可以對消息做操作;
  • onAcknowledgement:消息發送到Broker之後,或發送過程失敗時調用;
  • close:關閉攔截器調用,執行一些資源清理工作;

注意:這裏說的攔截器是針對消息發送流程。

2、自定義攔截

定義方式:實現ProducerInterceptor接口即可。

攔截器一:在onSend方法中,對攔截的消息進行修改。

@Component
public class SendStartInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendStartInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        // 修改消息內容
        return new ProducerRecord<>(record.topic(), record.partition(),
                                    record.timestamp(), record.key(),
                              "onSend:{" + record.value()+"}");
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        LOGGER.info("onAcknowledgement...");
    }
    @Override
    public void close() {
        LOGGER.info("SendStart close...");
    }
}

攔截器二:在onAcknowledgement方法中,判斷消息是否發送成功。

@Component
public class SendOverInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendOverInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        LOGGER.info("record...{}", record.value());
        return record ;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception != null){
            LOGGER.info("Send Fail...exe-msg",exception.getMessage());
        }
        LOGGER.info("Send success...");
    }

    @Override
    public void close() {
        LOGGER.info("SendOver close...");
    }
}

加載攔截器:基於一個KafkaProducer配置Bean,加入攔截器。

@Configuration
public class KafkaConfig {

    @Bean
    public Producer producer (){
        Properties props = new Properties();
        // 省略其他配置...
        // 添加攔截器
        List<String> interceptors = new ArrayList<>();
        interceptors.add("com.kafka.cluster.interceptor.SendStartInterceptor");
        interceptors.add("com.kafka.cluster.interceptor.SendOverInterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
        return new KafkaProducer<>(props) ;
    }
}

3、代碼案例

@RestController
public class SendMsgWeb {
    @Resource
    private KafkaProducer<String,String> producer ;
    @GetMapping("/sendMsg")
    public String sendMsg (){
        producer.send(new ProducerRecord<>("one-topic", "msgKey", "msgValue"));
        return "success" ;
    }
}

基於上述自定義Bean類型,進行消息發送,關注攔截器中打印日誌信息。

三、Kafka存儲分析

說明:該過程基於上述案例producer.send方法追蹤的源碼執行流程,源碼中的過程相對清楚,涉及的核心流程如下。

1、消息生成過程

Producer發送消息採用的是異步發送的方式,消息發送過程如下:

  • Producer發送消息之後,經過攔截器,序列化,事務判斷;
  • 流程執行后,消息內容放入容器中;
  • 容器在指定時間內如果裝滿(size),會喚醒Sender線程;
  • 容器如果在指定時間內沒有裝滿,也會執行一次Sender線程喚醒;
  • 喚醒Sender線程之後,把容器數據拉取到topic中;

絮叨一句:讀這些中間件的源碼,不僅能開闊思維,也會讓自己意識到平時寫的代碼可能真的叫搬磚。

2、存儲機制

Kafka中消息是以topic進行標識分類,生產者面向topic生產消息,topic分區(partition)是物理上的存儲,基於消息日誌文件的方式。

  • 每個partition對應於一個log文件,發送的消息不斷追加到該log文件末端;
  • log文件中存儲的就是producer生產的消息數據,採用分片和索引機制;
  • partition分為多個segment。每個segment對應兩個(.index)和(.log)文件;
  • index文件類型存儲的索引信息;
  • log文件存儲消息的數據;
  • 索引文件中的元數據指向對應數據文件中message的物理偏移地址;
  • 消費者組中的每個消費者,都會實時記錄消費的消息offset位置;
  • 當然消息消費出錯時,恢復是從上次的記錄位置繼續消費;

3、事務控制機制

Kafka支持消息的事務控制

Producer事務

跨分區跨會話的事務原理,引入全局唯一的TransactionID,並將Producer獲得的PID和TransactionID綁定。Producer重啟后可以通過正在進行的TransactionID獲得原來的PID。
Kafka基於TransactionCoordinator組件管理Transaction,Producer通過和TransactionCoordinator交互獲得TransactionID對應的任務狀態。TransactionCoordinator將事務狀態寫入Kafka的內部Topic,即使整個服務重啟,進行中的事務狀態可以得到恢復。

Consumer事務

Consumer消息消費,事務的保證強度很低,無法保證消息被精確消費,因為同一事務的消息可能會出現重啟后已經被刪除的情況。

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

推薦關聯閱讀:數據源管理系列

序號 標題
01 數據源管理:主從庫動態路由,AOP模式讀寫分離
02 數據源管理:基於JDBC模式,適配和管理動態數據源
03 數據源管理:動態權限校驗,表結構和數據遷移流程
04 數據源管理:關係型分庫分表,列式庫分佈式計算
05 數據源管理:PostGreSQL環境整合,JSON類型應用
06 數據源管理:基於DataX組件,同步數據和源碼分析
07 數據源管理:OLAP查詢引擎,ClickHouse集群化管理

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

akka-typed(6) – cluster:group router, cluster-load-balancing

先談談akka-typed的router actor。route 分pool router, group router兩類。我們先看看pool-router的使用示範:

      val pool = Routers.pool(poolSize = 4)( // make sure the workers are restarted if they fail
 Behaviors.supervise(WorkerRoutee()).onFailure[Exception](SupervisorStrategy.restart)) val router = ctx.spawn(pool, "worker-pool") (0 to 10).foreach { n => router ! WorkerRoutee.DoLog(s"msg $n")
      }

上面例子里的pool是個pool-router,意思是一個有4個routees的routee池。每個routee都是通過WorkerRoutee()構建的,意味着routee池中只有一個種類的actor。pool-router是通過工廠方法直接在本地(JVM)構建(spawn)所有的routee。也就是說所有routee都是router的子actor。

再看看group-router的使用例子:

val serviceKey = ServiceKey[Worker.Command]("log-worker") // this would likely happen elsewhere - if we create it locally we // can just as well use a pool
      val workerRoutee = ctx.spawn(WorkerRoutee(), "worker-route") ctx.system.receptionist ! Receptionist.Register(serviceKey, workerRoutee) val group = Routers.group(serviceKey) val router = ctx.spawn(group, "worker-group") // the group router will stash messages until it sees the first listing of registered // services from the receptionist, so it is safe to send messages right away
      (0 to 10).foreach { n => router ! WorkerRoutee.DoLog(s"msg $n") }

group-router與pool-router有較多分別:

1、routee是在router之外構建的,router是用一個key通過Receptionist獲取同key的actor清單作為routee group的

2、Receptionist是集群全局的。任何節點上的actor都可以發送註冊消息在Receptionist上登記

3、沒有size限制,任何actor一旦在Receptionist上登記即變成routee,接受router管理

應該說如果想把運算任務分配在集群里的各節點上并行運算實現load-balance效果,group-router是最合適的選擇。不過對不同的運算任務需要多少routee則需要用戶自行決定,不像以前akka-classic里通過cluster-metrics根據節點負載情況自動增減routee實例那麼方便。

Receptionist: 既然說到,那麼就再深入一點介紹Receptionist的應用:上面提到,Receptionist是集群全局的。就是說任何節點上的actor都可以在Receptonist上註冊形成一個生存在集群中不同節點的actor清單。如果Receptionist把這個清單提供給一個用戶,那麼這個用戶就可以把運算任務配置到各節點上,實現某種意義上的分佈式運算模式。Receptionist的使用方式是:通過向本節點的Receptionist發送消息去登記ActorRef,然後通過Receptionist發布的登記變化消息即可獲取最新的ActorRef清單:

  val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker") ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self) ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter)

Receptionist的登記和清單獲取是以ServiceKey作為關聯的。那麼獲取的清單內應該全部是一種類型的actor,只不過它們的地址可能是跨節點的,但它們只能進行同一種運算。從另一個角度說,一項任務是分佈在不同節點的actor并行進行運算的。

在上篇討論里提過:如果發布-訂閱機制是在兩個actor之間進行的,那麼這兩個actor也需要在規定的信息交流協議框架下作業:我們必須注意消息類型,提供必要的消息類型轉換機制。下面是一個Receptionist登記示範:

object Worker { val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker") sealed trait Command final case class TransformText(text: String, replyTo: ActorRef[TextTransformed]) extends Command with CborSerializable final case class TextTransformed(text: String) extends CborSerializable def apply(): Behavior[Command] = Behaviors.setup { ctx =>
      // each worker registers themselves with the receptionist
      ctx.log.info("Registering myself with receptionist") ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self) Behaviors.receiveMessage { case TransformText(text, replyTo) => replyTo ! TextTransformed(text.toUpperCase) Behaviors.same } } }

Receptionist登記比較直接:登記者不需要Receptionist返回消息,所以隨便用ctx.self作為消息的sender。注意TransformText的replyTo: ActorRef[TextTransformed],代表sender是個可以處理TextTransformed消息類型的actor。實際上,在sender方是通過ctx.ask提供了TextTransformed的類型轉換。

Receptionist.Subscribe需要Receptionist返回一個actor清單,所以是個request/response模式。那麼發送給Receptionist消息中的replyTo必須是發送者能處理的類型,如下:

  def apply(): Behavior[Event] = Behaviors.setup { ctx => Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] { case Worker.WorkerServiceKey.Listing(workers) => WorkersUpdated(workers) } ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter) ... } 

ctx.messageAdapter進行了一個從Receptionist.Listing返回類型到WorkersUpdated類型的轉換機制登記:從Receptionist回復的List類型會被轉換成WorkersUpdated類型,如下:

... Behaviors.receiveMessage { case WorkersUpdated(newWorkers) => ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers) ...

另外,上面提過的TextTransformed轉換如下:

 ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) { case Success(transformedText) => TransformCompleted(transformedText.text, text) case Failure(ex) => JobFailed("Processing timed out", text) }

ctx.ask將TextTransformed轉換成TransformCompleted。完整的Behavior定義如下:

object Frontend { sealed trait Event private case object Tick extends Event private final case class WorkersUpdated(newWorkers: Set[ActorRef[Worker.TransformText]]) extends Event private final case class TransformCompleted(originalText: String, transformedText: String) extends Event private final case class JobFailed(why: String, text: String) extends Event def apply(): Behavior[Event] = Behaviors.setup { ctx => Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] { case Worker.WorkerServiceKey.Listing(workers) => WorkersUpdated(workers) } ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter) timers.startTimerWithFixedDelay(Tick, Tick, 2.seconds) running(ctx, IndexedSeq.empty, jobCounter = 0) } } private def running(ctx: ActorContext[Event], workers: IndexedSeq[ActorRef[Worker.TransformText]], jobCounter: Int): Behavior[Event] = Behaviors.receiveMessage { case WorkersUpdated(newWorkers) => ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers) running(ctx, newWorkers.toIndexedSeq, jobCounter) case Tick =>
        if (workers.isEmpty) { ctx.log.warn("Got tick request but no workers available, not sending any work") Behaviors.same } else { // how much time can pass before we consider a request failed
          implicit val timeout: Timeout = 5.seconds val selectedWorker = workers(jobCounter % workers.size) ctx.log.info("Sending work for processing to {}", selectedWorker) val text = s"hello-$jobCounter" ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) { case Success(transformedText) => TransformCompleted(transformedText.text, text) case Failure(ex) => JobFailed("Processing timed out", text) } running(ctx, workers, jobCounter + 1) } case TransformCompleted(originalText, transformedText) => ctx.log.info("Got completed transform of {}: {}", originalText, transformedText) Behaviors.same case JobFailed(why, text) => ctx.log.warn("Transformation of text {} failed. Because: {}", text, why) Behaviors.same }

現在我們可以示範用group-router來實現某種跨節點的分佈式運算。因為group-router是通過Receptionist來實現對routees管理的,而Receptionist是集群全局的,意味着如果我們在各節點上構建routee,然後向Receptionist登記,就會形成一個跨節點的routee ActorRef清單。如果把任務分配到這個清單上的routee上去運算,應該能實現集群節點負載均衡的效果。下面我們就示範這個loadbalancer。流程很簡單:在一個接入點 (serviceActor)中構建workersRouter,然後3個workerRoutee並向Receptionist登記,把接到的任務分解成子任務逐個發送給workersRouter。每個workerRoutee完成任務后將結果發送給一個聚合器Aggregator,Aggregator在核對完成接收所有workerRoutee返回的結果后再把匯總結果返回serverActor。先看看這個serverActor:

object Service { val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router") sealed trait Command extends CborSerializable case class ProcessText(text: String) extends Command { require(text.nonEmpty) } case class WrappedResult(res: Aggregator.Response) extends Command def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx => val aggregator = ctx.spawn(Aggregator(), "aggregator") val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult) Behaviors.receiveMessage { case ProcessText(text) => ctx.log.info("******************** received ProcessText command: {} ****************",text) val words = text.split(' ').toIndexedSeq aggregator ! Aggregator.CountText(words.size, aggregatorRef) words.foreach { word => workersRouter ! WorkerRoutee.Count(word, aggregator) } Behaviors.same case WrappedResult(msg) => msg match { case Aggregator.Result(res) => ctx.log.info("************** mean length of words = {} **********", res) } Behaviors.same } } def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = { val singletonSettings = ClusterSingletonSettings(ctx.system) .withRole("front") SingletonActor( Behaviors.supervise( serviceBehavior(workersRouter) ).onFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ) , "singletonActor" ).withSettings(singletonSettings) } def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx => val cluster = Cluster(ctx.system) val workersRouter = ctx.spawn( Routers.group(routerServiceKey) .withRoundRobinRouting(), "workersRouter" ) (0 until 3).foreach { n => val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n") ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee) } val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter)) Behaviors.receiveMessage { case job@ProcessText(text) => singletonActor ! job Behaviors.same } } }

整體goup-router和routee的構建是在apply()里,並把接到的任務轉發給singletonActor。singletonActor是以serviceBehavior為核心的一個actor。在servceBehavior里把收到的任務分解並分別發送給workersRouter。值得注意的是:serviceBehavior期望接收從Aggregator的回應,它們之間存在request/response模式信息交流,所以需要Aggregator.Response到WrappedResult的類型轉換機制。還有:子任務是通過workersRoute發送給個workerRoutee的,我們需要各workerRoutee把運算結果返給給Aggregator,所以發送給workersRouter的消息包含了Aggregator的ActorRef,如:workersRouter ! WorkerRoutee.Count(cnt,aggregatorRef)。

Aggregator是個persistentActor, 如下:

 

object Aggregator { sealed trait Command sealed trait Event extends CborSerializable sealed trait Response case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command case class MarkLength(word: String, len: Int) extends Command case class TextCounted(cnt: Int) extends Event case class LengthMarked(word: String, len: Int) extends Event case class Result(meanWordLength: Double) extends Response case class State(expectedNum: Int = 0, lens: List[Int] = Nil) var replyTo: ActorRef[Response] = _ def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => { cmd match { case CountText(cnt,ref) => replyTo = ref Effect.persist(TextCounted(cnt)) case MarkLength(word,len) => Effect.persist(LengthMarked(word,len)) } } def eventHandler: (State, Event) => State = (st,ev) => { ev match { case TextCounted(cnt) => st.copy(expectedNum = cnt, lens = Nil) case LengthMarked(word,len) => val state = st.copy(lens = len :: st.lens) if (state.lens.size >= state.expectedNum) { val meanWordLength = state.lens.sum.toDouble / state.lens.size replyTo ! Result(meanWordLength) State() } else state } } val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => { if (st.lens.isEmpty) { if (ev.isInstanceOf[LengthMarked]) true
          else
            false } else
         false } def apply(): Behavior[Command] = Behaviors.supervise( Behaviors.setup[Command] { ctx => EventSourcedBehavior( persistenceId = PersistenceId("33","2333"), emptyState = State(), commandHandler = commandHandler, eventHandler = eventHandler ).onPersistFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ).receiveSignal { case (state, RecoveryCompleted) => ctx.log.info("**************Recovery Completed with state: {}***************",state) case (state, SnapshotCompleted(meta))  => ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr) case (state,RecoveryFailed(err)) => ctx.log.error("*************recovery failed with: {}***************",err.getMessage) case (state,SnapshotFailed(meta,err)) => ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage) }.snapshotWhen(takeSnapShot) } ).onFailure( SupervisorStrategy .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1) .withMaxRestarts(3) .withResetBackoffAfter(10.seconds) ) }

注意這個takeSnapShot函數:這個函數是在EventSourcedBehavior.snapshotWhen(takeSnapShot)調用的。傳入參數是(State,Event,seqenceNr),我們需要對State,Event的當前值進行分析后返回true代表做一次snapshot。

看看一部分显示就知道任務已經分配到幾個節點上的routee:

20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.WorkerRoutee$ - ************** processing [this] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [text] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-36] INFO com.learn.akka.WorkerRoutee$ - ************** processing [be] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-16] INFO com.learn.akka.WorkerRoutee$ - ************** processing [will] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-26] INFO com.learn.akka.WorkerRoutee$ - ************** processing [is] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-13] INFO com.learn.akka.WorkerRoutee$ - ************** processing [the] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [that] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [analyzed] on akka://ClusterSystem@127.0.0.1:25251 ***********

這個例子的源代碼如下:

package com.learn.akka

import akka.actor.typed._
import akka.persistence.typed._
import akka.persistence.typed.scaladsl._
import scala.concurrent.duration._
import akka.actor.typed.receptionist._
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl._
import akka.cluster.typed.Cluster
import akka.cluster.typed.ClusterSingleton
import akka.cluster.typed.ClusterSingletonSettings
import akka.cluster.typed.SingletonActor
import com.typesafe.config.ConfigFactory

object WorkerRoutee {
  sealed trait Command extends CborSerializable
  case class Count(word: String, replyTo: ActorRef[Aggregator.Command]) extends Command

  def apply(nodeAddress: String): Behavior[Command] = Behaviors.setup {ctx =>
    Behaviors.receiveMessage[Command] {
      case Count(word,replyTo) =>
        ctx.log.info("************** processing [{}] on {} ***********",word,nodeAddress)
        replyTo ! Aggregator.MarkLength(word,word.length)
        Behaviors.same
    }
  }
}
object Aggregator {
  sealed trait Command
  sealed trait Event extends  CborSerializable
  sealed trait Response

  case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command
  case class MarkLength(word: String, len: Int) extends Command
  case class TextCounted(cnt: Int) extends Event
  case class LengthMarked(word: String, len: Int) extends Event
  case class Result(meanWordLength: Double) extends Response

  case class State(expectedNum: Int = 0, lens: List[Int] = Nil)

  var replyTo: ActorRef[Response] = _

  def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => {
    cmd match {
      case CountText(cnt,ref) =>
        replyTo = ref
        Effect.persist(TextCounted(cnt))
      case MarkLength(word,len) =>
        Effect.persist(LengthMarked(word,len))
    }
  }
  def eventHandler: (State, Event) => State = (st,ev) => {
    ev match {
      case TextCounted(cnt) =>
        st.copy(expectedNum = cnt, lens = Nil)
      case LengthMarked(word,len) =>
        val state = st.copy(lens = len :: st.lens)
        if (state.lens.size >= state.expectedNum) {
          val meanWordLength = state.lens.sum.toDouble / state.lens.size
          replyTo ! Result(meanWordLength)
          State()
        } else state
    }
  }
  val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => {
      if (st.lens.isEmpty) {
          if (ev.isInstanceOf[LengthMarked])
            true
          else
            false
      } else
         false
  }
  def apply(): Behavior[Command] = Behaviors.supervise(
    Behaviors.setup[Command] { ctx =>
      EventSourcedBehavior(
        persistenceId = PersistenceId("33","2333"),
        emptyState = State(),
        commandHandler = commandHandler,
        eventHandler = eventHandler
      ).onPersistFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      ).receiveSignal {
        case (state, RecoveryCompleted) =>
          ctx.log.info("**************Recovery Completed with state: {}***************",state)
        case (state, SnapshotCompleted(meta))  =>
          ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
        case (state,RecoveryFailed(err)) =>
          ctx.log.error("*************recovery failed with: {}***************",err.getMessage)
        case (state,SnapshotFailed(meta,err)) =>
          ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage)
      }.snapshotWhen(takeSnapShot)
    }
  ).onFailure(
    SupervisorStrategy
      .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
      .withMaxRestarts(3)
      .withResetBackoffAfter(10.seconds)
  )
}
object Service {
  val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router")

  sealed trait Command extends CborSerializable

  case class ProcessText(text: String) extends Command {
    require(text.nonEmpty)
  }

  case class WrappedResult(res: Aggregator.Response) extends Command

  def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val aggregator = ctx.spawn(Aggregator(), "aggregator")
    val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult)
    Behaviors.receiveMessage {
      case ProcessText(text) =>
        ctx.log.info("******************** received ProcessText command: {} ****************",text)
        val words = text.split(' ').toIndexedSeq
        aggregator ! Aggregator.CountText(words.size, aggregatorRef)
        words.foreach { word =>
          workersRouter ! WorkerRoutee.Count(word, aggregator)
        }
        Behaviors.same
      case WrappedResult(msg) =>
        msg match {
          case Aggregator.Result(res) =>
            ctx.log.info("************** mean length of words = {} **********", res)
        }
        Behaviors.same
    }
  }

  def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = {
    val singletonSettings = ClusterSingletonSettings(ctx.system)
      .withRole("front")
    SingletonActor(
      Behaviors.supervise(
        serviceBehavior(workersRouter)
      ).onFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      )
      , "singletonActor"
    ).withSettings(singletonSettings)
  }

  def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val cluster = Cluster(ctx.system)
    val workersRouter = ctx.spawn(
      Routers.group(routerServiceKey)
        .withRoundRobinRouting(),
      "workersRouter"
    )
    (0 until 3).foreach { n =>
      val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n")
      ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee)
    }
    val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter))
    Behaviors.receiveMessage {
      case job@ProcessText(text) =>
        singletonActor ! job
        Behaviors.same
    }
  }

}

object LoadBalance {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty) {
      startup("compute", 25251)
      startup("compute", 25252)
      startup("compute", 25253)
      startup("front", 25254)
    } else {
      require(args.size == 2, "Usage: role port")
      startup(args(0), args(1).toInt)
    }
  }

  def startup(role: String, port: Int): Unit = {
    // Override the configuration of the port when specified as program argument
    val config = ConfigFactory
      .parseString(s"""
      akka.remote.artery.canonical.port=$port
      akka.cluster.roles = [$role]
      """)
      .withFallback(ConfigFactory.load("cluster-persistence"))

    val frontEnd = ActorSystem[Service.Command](Service(), "ClusterSystem", config)
    if (role == "front") {
      println("*************** sending ProcessText command  ************")
      frontEnd ! Service.ProcessText("this is the text that will be analyzed")
    }

  }

}

cluster-persistence.conf

akka.actor.allow-java-serialization = on
akka {
  loglevel = INFO
  actor {
    provider = cluster
    serialization-bindings {
      "com.learn.akka.CborSerializable" = jackson-cbor
    }
  }
 remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 0
    }
  }
  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:25251",
      "akka://ClusterSystem@127.0.0.1:25252"]
  }
  # use Cassandra to store both snapshots and the events of the persistent actors
  persistence {
    journal.plugin = "akka.persistence.cassandra.journal"
    snapshot-store.plugin = "akka.persistence.cassandra.snapshot"
  }
}
akka.persistence.cassandra {
  # don't use autocreate in production
  journal.keyspace = "poc"
  journal.keyspace-autocreate = on
  journal.tables-autocreate = on
  snapshot.keyspace = "poc_snapshot"
  snapshot.keyspace-autocreate = on
  snapshot.tables-autocreate = on
}

datastax-java-driver {
  basic.contact-points = ["192.168.11.189:9042"]
  basic.load-balancing-policy.local-datacenter = "datacenter1"
}

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.1"
scalacOptions in Compile ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlog-reflective-calls", "-Xlint")
javacOptions in Compile ++= Seq("-Xlint:unchecked", "-Xlint:deprecation")

val AkkaVersion = "2.6.5"
val AkkaPersistenceCassandraVersion = "1.0.0"


libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-cluster-sharding-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-query" % AkkaVersion,
  "com.typesafe.akka" %% "akka-serialization-jackson" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-cassandra" % AkkaPersistenceCassandraVersion,
  "com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
  "ch.qos.logback"     % "logback-classic"             % "1.2.3"
)

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

實時標籤開發——從零開始搭建實時用戶畫像(五)

數據接入

數據的接入可以通過將數據實時寫入Kafka進行接入,不管是直接的寫入還是通過oracle和mysql的實時接入方式,比如oracle的ogg,mysql的binlog

ogg

Golden Gate(簡稱OGG)提供異構環境下交易數據的實時捕捉、變換、投遞。

通過OGG可以實時的將oracle中的數據寫入Kafka中。

對生產系統影響小:實時讀取交易日誌,以低資源佔用實現大交易量數據實時複製

以交易為單位複製,保證交易一致性:只同步已提交的數據

高性能

  • 智能的交易重組和操作合併
  • 使用數據庫本地接口訪問
  • 并行處理體系

binlog

MySQL 的二進制日誌 binlog 可以說是 MySQL 最重要的日誌,它記錄了所有的 DDLDML 語句(除了數據查詢語句select、show等),以事件形式記錄,還包含語句所執行的消耗的時間,MySQL的二進制日誌是事務安全型的。binlog 的主要目的是複製和恢復

通過這些手段,可以將數據同步到kafka也就是我們的實時系統中來。

Flink接入Kafka數據

Apache Kafka Connector可以方便對kafka數據的接入。

依賴

<dependency>  <groupId>org.apache.flink</groupId>  <artifactId>flink-connector-kafka_2.11</artifactId>  <version>1.9.0</version></dependency>
構建FlinkKafkaConsumer

必須有的:

1.topic名稱

2.用於反序列化Kafka數據的DeserializationSchema / KafkaDeserializationSchema

3.配置參數:“bootstrap.servers” “group.id” (kafka0.8還需要 “zookeeper.connect”)

val properties = new Properties()properties.setProperty("bootstrap.servers", "localhost:9092")// only required for Kafka 0.8properties.setProperty("zookeeper.connect", "localhost:2181")properties.setProperty("group.id", "test")stream = env    .addSource(new FlinkKafkaConsumer[String]("topic", new SimpleStringSchema(), properties))    .print()
時間戳和水印

在許多情況下,記錄的時間戳(顯式或隱式)嵌入記錄本身。另外,用戶可能想要周期性地或以不規則的方式發出水印。

我們可以定義好Timestamp Extractors / Watermark Emitters,通過以下方式將其傳遞給消費者

val env = StreamExecutionEnvironment.getExecutionEnvironment()val myConsumer = new FlinkKafkaConsumer[String](...)myConsumer.setStartFromEarliest()      // start from the earliest record possiblemyConsumer.setStartFromLatest()        // start from the latest recordmyConsumer.setStartFromTimestamp(...)  // start from specified epoch timestamp (milliseconds)myConsumer.setStartFromGroupOffsets()  // the default behaviour//指定位置//val specificStartOffsets = new java.util.HashMap[KafkaTopicPartition, java.lang.Long]()//specificStartOffsets.put(new KafkaTopicPartition("myTopic", 0), 23L)//myConsumer.setStartFromSpecificOffsets(specificStartOffsets)val stream = env.addSource(myConsumer)
檢查點

啟用Flink的檢查點后,Flink Kafka Consumer將使用主題中的記錄,並以一致的方式定期檢查其所有Kafka偏移以及其他操作的狀態。如果作業失敗,Flink會將流式程序恢復到最新檢查點的狀態,並從存儲在檢查點中的偏移量開始重新使用Kafka的記錄。

如果禁用了檢查點,則Flink Kafka Consumer依賴於內部使用的Kafka客戶端的自動定期偏移提交功能。

如果啟用了檢查點,則Flink Kafka Consumer將在檢查點完成時提交存儲在檢查點狀態中的偏移量。

val env = StreamExecutionEnvironment.getExecutionEnvironment()env.enableCheckpointing(5000) // checkpoint every 5000 msecs

Flink消費Kafka完整代碼:

import org.apache.flink.api.common.serialization.SimpleStringSchema;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;import java.util.Properties;public class KafkaConsumer {    public static void main(String[] args) throws Exception {        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();        Properties properties = new Properties();        properties.setProperty("bootstrap.servers", "localhost:9092");        properties.setProperty("group.id", "test");        //構建FlinkKafkaConsumer        FlinkKafkaConsumer<String> myConsumer = new FlinkKafkaConsumer<>("topic", new SimpleStringSchema(), properties);        //指定偏移量        myConsumer.setStartFromEarliest();        DataStream<String> stream = env                .addSource(myConsumer);        env.enableCheckpointing(5000);        stream.print();        env.execute("Flink Streaming Java API Skeleton");    }

這樣數據已經實時的接入我們系統中,可以在Flink中對數據進行處理了,那麼如何對標籤進行計算呢? 標籤的計算過程極大的依賴於數據倉庫的能力,所以擁有了一個好的數據倉庫,標籤也就很容易計算出來了。

數據倉庫基礎知識

數據倉庫是指一個面向主題的、集成的、穩定的、隨時間變化的數據的集合,以用於支持管理決策的過程。

(1)面向主題
業務數據庫中的數據主要針對事物處理任務,各個業務系統之間是各自分離的。而數據倉庫中的數據是按照一定的主題進行組織的

(2)集成
數據倉庫中存儲的數據是從業務數據庫中提取出來的,但並不是原有數據的簡單複製,而是經過了抽取、清理、轉換(ETL)等工作。
業務數據庫記錄的是每一項業務處理的流水賬,這些數據不適合於分析處理,進入數據倉庫之前需要經過系列計算,同時拋棄一些分析處理不需要的數據。

(3)穩定
操作型數據庫系統中一般只存儲短期數據,因此其數據是不穩定的,記錄的是系統中數據變化的瞬態。
數據倉庫中的數據大多表示過去某一時刻的數據,主要用於查詢、分析,不像業務系統中數據庫一樣經常修改。一般數據倉庫構建完成,主要用於訪問

OLTP 聯機事務處理
OLTP是傳統關係型數據庫的主要應用,主要用於日常事物、交易系統的處理
1、數據量存儲相對來說不大
2、實時性要求高,需要支持事物
3、數據一般存儲在關係型數據庫(oracle或mysql中)

OLAP 聯機分析處理
OLAP是數據倉庫的主要應用,支持複雜的分析查詢,側重決策支持
1、實時性要求不是很高,ETL一般都是T+1的數據;
2、數據量很大;
3、主要用於分析決策;

星形模型是最常用的數據倉庫設計結構。由一個事實表和一組維表組成,每個維表都有一個維主鍵。
該模式核心是事實表,通過事實表將各種不同的維表連接起來,各個維表中的對象通過事實表與另一個維表中的對象相關聯,這樣建立各個維表對象之間的聯繫
維表:用於存放維度信息,包括維的屬性和層次結構;
事實表:是用來記錄業務事實並做相應指標統計的表。同維表相比,事實表記錄數量很多

雪花模型是對星形模型的擴展,每一個維表都可以向外連接多個詳細類別表。除了具有星形模式中維表的功能外,還連接對事實表進行詳細描述的維度,可進一步細化查看數據的粒度
例如:地點維表包含屬性集{location_id,街道,城市,省,國家}。這種模式通過地點維度表的city_id與城市維度表的city_id相關聯,得到如{101,“解放大道10號”,“武漢”,“湖北省”,“中國”}、{255,“解放大道85號”,“武漢”,“湖北省”,“中國”}這樣的記錄。
星形模型是最基本的模式,一個星形模型有多個維表,只存在一個事實表。在星形模式的基礎上,用多個表來描述一個複雜維,構造維表的多層結構,就得到雪花模型

清晰數據結構:每一個數據分層都有它的作用域,這樣我們在使用表的時候能更方便地定位和理解
臟數據清洗:屏蔽原始數據的異常
屏蔽業務影響:不必改一次業務就需要重新接入數據
數據血緣追蹤:簡單來講可以這樣理解,我們最終給業務呈現的是能直接使用的一張業務表,但是它的來源有很多,如果有一張來源表出問題了,我們希望能夠快速準確地定位到問題,並清楚它的危害範圍。
減少重複開發:規範數據分層,開發一些通用的中間層數據,能夠減少極大的重複計算。
把複雜問題簡單化。將一個複雜的任務分解成多個步驟來完成,每一層只處理單一的步驟,比較簡單和容易理解。便於維護數據的準確性,當數據出現問題之後,可以不用修復所有的數據,只需要從有問題的步驟開始修復。

數據倉庫的數據直接對接OLAP或日誌類數據,
用戶畫像只是站在用戶的角度,對數據倉庫數據做進一步的建模加工。因此每天畫像標籤相關數據的調度依賴上游數據倉庫相關任務執行完成。

在了解了數據倉庫以後,我們就可以進行標籤的計算了。在開發好標籤的邏輯以後,將數據寫入hive和druid中,完成實時與離線的標籤開發工作。

Flink Hive Druid

flink hive

Flink從1.9開始支持集成Hive,不過1.9版本為beta版,不推薦在生產環境中使用。在最新版Flink1.10版本,標志著對 Blink的整合宣告完成,隨着對 Hive 的生產級別集成,Hive作為數據倉庫系統的絕對核心,承擔著絕大多數的離線數據ETL計算和數據管理,期待Flink未來對Hive的完美支持。

而 HiveCatalog 會與一個 Hive Metastore 的實例連接,提供元數據持久化的能力。要使用 Flink 與 Hive 進行交互,用戶需要配置一個 HiveCatalog,並通過 HiveCatalog 訪問 Hive 中的元數據。

添加依賴

要與Hive集成,需要在Flink的lib目錄下添加額外的依賴jar包,以使集成在Table API程序或SQL Client中的SQL中起作用。或者,可以將這些依賴項放在文件夾中,並分別使用Table API程序或SQL Client 的-C-l選項將它們添加到classpath中。本文使用第一種方式,即將jar包直接複製到$FLINK_HOME/lib目錄下。本文使用的Hive版本為2.3.4(對於不同版本的Hive,可以參照官網選擇不同的jar包依賴),總共需要3個jar包,如下:

  • flink-connector-hive_2.11-1.10.0.jar
  • flink-shaded-hadoop-2-uber-2.7.5-8.0.jar
  • hive-exec-2.3.4.jar

添加Maven依賴

<!-- Flink Dependency -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-connector-hive_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-java-bridge_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

<!-- Hive Dependency -->
<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-exec</artifactId>
    <version>${hive.version}</version>
    <scope>provided</scope>
</dependency>

實例代碼

package com.flink.sql.hiveintegration;

import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.catalog.hive.HiveCatalog;


public class FlinkHiveIntegration {

    public static void main(String[] args) throws Exception {

        EnvironmentSettings settings = EnvironmentSettings
                .newInstance()
                .useBlinkPlanner() // 使用BlinkPlanner
                .inBatchMode() // Batch模式,默認為StreamingMode
                .build();

        //使用StreamingMode
       /* EnvironmentSettings settings = EnvironmentSettings
                .newInstance()
                .useBlinkPlanner() // 使用BlinkPlanner
                .inStreamingMode() // StreamingMode
                .build();*/

        TableEnvironment tableEnv = TableEnvironment.create(settings);

        String name = "myhive";      // Catalog名稱,定義一個唯一的名稱表示
        String defaultDatabase = "qfbap_ods";  // 默認數據庫名稱
        String hiveConfDir = "/opt/modules/apache-hive-2.3.4-bin/conf";  // hive-site.xml路徑
        String version = "2.3.4";       // Hive版本號

        HiveCatalog hive = new HiveCatalog(name, defaultDatabase, hiveConfDir, version);

        tableEnv.registerCatalog("myhive", hive);
        tableEnv.useCatalog("myhive");
        // 創建數據庫,目前不支持創建hive表
        String createDbSql = "CREATE DATABASE IF NOT EXISTS myhive.test123";

        tableEnv.sqlUpdate(createDbSql);  

    }
}

Flink Druid

可以將Flink分析好的數據寫回kafka,然後在druid中接入數據,也可以將數據直接寫入druid,以下為示例代碼:

依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.flinkdruid</groupId>
    <artifactId>FlinkDruid</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>FlinkDruid</name>
    <description>Flink Druid Connection</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

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

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

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-core</artifactId>
            <version>1.9.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.12</artifactId>
            <version>1.9.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

示例代碼

@SpringBootApplication
public class FlinkDruidApp {


    private static String url = "http://localhost:8200/v1/post/wikipedia";

    private static RestTemplate template;

    private static HttpHeaders headers;


    FlinkDruidApp() {

        template = new RestTemplate();
        headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);

    }

    public static void main(String[] args) throws Exception {

        SpringApplication.run(FlinkDruidApp.class, args);

        // Creating Flink Execution Environment
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

        //Define data source
        DataSet<String> data = env.readTextFile("/wikiticker-2015-09-12-sampled.json");

        // Trasformation on the data 
        data.map(x -> {

            return httpsPost(x).toString();
        }).print();


    }

    // http post method to post data in Druid
    private static ResponseEntity<String> httpsPost(String json) {

        HttpEntity<String> requestEntity =
                new HttpEntity<>(json, headers);
        ResponseEntity<String> response =
                template.exchange("http://localhost:8200/v1/post/wikipedia", HttpMethod.POST, requestEntity,
                        String.class);

        return response;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    } 

}

標籤的開發工作繁瑣,需要不斷的開發並且優化,但是如何將做好的標籤提供出去產生真正的價值呢? 下一章,我們將介紹用戶畫像產品化,未完待續~

參考文獻

《用戶畫像:方法論與工程化解決方案》

更多實時數據分析相關博文與科技資訊,歡迎關注 “實時流式計算” 獲取用戶畫像相關資料 請關注 “實時流式計算” 回復 “用戶畫像”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

Telegraf和Grafana監控多平台上的SQL Server-自定義監控數據收集

問題

在上一篇文章中,我們使用Telegraf自帶的Plugin配置好了的監控,但是自帶的Plugin並不能完全覆蓋我們想要的監控指標,就需要收集額外的自定義的監控數據,實現的方法有:

  • 開發自己的Telegraf Plugin

  • 使用可以執行自定義腳本的inputs plugin

此處收集的監控項不多,收集間隔也不是很頻繁,所以我選擇Telegraf預置的Inputs.exec plugin實現。它非常靈活,可以執行任意命令和腳本。在腳本中實現獲取監控數據的邏輯,然後使用inputs.exec執行。獲取數據之後,需要按InfluxDB Line Protocol 格式組織數據,才能寫入到Influxdb。這種格式的組織方式:

[measurement],[tags] [fields] [timestamp]

  • measurement,類似於SQL中表的概念,數據存放的容器

  • tags,K-V格式用於標記數據記錄,一般它們的值不經常變化,如主機名。同時tags上會建立索引,查詢效率會好一些.

  • fields,K-V格式,表示真正收集的不同時間點的數據項,如CPU Load

  • timestamp,UNIX 時間戳,Influxdb是時序數據庫,所有數據都要與時間關聯起來。

  • measurement和tag之間用逗號分隔,fields 與它們用空格(whitespace)分隔

不管是運行在Linux還是Windows上的SQL,通常使用T-SQL查詢實例內部的數據和使用操作系統腳本查詢實例外部的數據以實現監控。接下來,以執行T-SQL獲取自定義監控數據為例,看看在Windos和Linux上分別如何實現。

解決方案

首先在被監控的實例上把相應的邏輯寫成存儲過程,然後通過inputs.exec調用之。

例如我在目標實例的influx庫中創建了一個存儲過程influx.usp_getInstanceInfo獲取一些實例的配置信息。然後需要在telegraf配置文件中啟用inputs.exec調用這個存儲過程。存儲過程的輸出數據如下:

sqlserver_property,host=SQL19N1,sql_instance=SQL19N1 host_platform="Linux",host_distribution="CentOS Linux",host_release=7,edition="Developer Edition (64-bit)",product_version="15.0.4033.1",collation="SQL_Latin1_General_CP1_CI_AS",is_clustered=f,is_hadr=t,cpu_count=2,scheduler_count=2,physical_memory_kb=6523904,max_workers_count=512,max_dop=0,max_memmory=2147483647 1590915136000000000

數據寫入到sqlserver_property,tags包括host,sql_instance,後面的全是fields。

  • SQL On Linux

使用SQLCMD調用存儲過程,把SQLCMD命令寫到一個bash文件中/telegraf/get_sqlproperty.sh

#!/bin/bash

/opt/mssql-tools/bin/sqlcmd -S SQL19N1 -U telegraf -P <yourpassword> -d influx -y 0 -Q "EXEC influx.usp_getInstanceInfo"

修改telegraf.conf中的inputs.exec, 然後重啟telegraf生效:

因為收集的是實例屬性信息,收集間隔設置的比較長。

 [[inputs.exec]]
#   ## Commands array
   commands = [
        "/telegraf/get_sqlproperty.sh"
        ]
        
   timeout = "5s"
   interval="24h"
   data_format = "influx"
  • SQL On Windows

Windows上首選使用PowerShell實現,把執行SQL的命令寫到C:\Monitoring\scripts\get_sqlproperty.ps1。col_res是存儲過程輸出的列名。

(Invoke-Sqlcmd -ServerInstance SQL17N1 -Username telegraf -Password "<yourpassword>" -Database influx -Query "exec influx.usp_getInstanceInfo" ).col_res

修改telegraf.conf中的inputs.exec, 然後重啟telegraf生效.

需要特別注意的問題:

  • 指定文件路徑時,要使用Linux路徑表達的forward slash(/), 而不是Windows中的 back slash(\)

  • ps1文件路徑使用單引號(single quote)

  • 避免文件路徑中有空格(whitespace)

 [[inputs.exec]]
#   ## Commands array
   commands = [
        "powershell 'C:/Monitoring/scripts/get_sqlproperty.ps1' "
        ]
        
   timeout = "5s"
   interval="24h"
   data_format = "influx"

配置完成后,看看measurement和數據:

總結

  • 在inputs.exec中最好是調用腳本,而不是命令。這樣當你需要變更數據收集邏輯,直接修改腳本即可,而不需要修改Telegraf的配置文件,避免重啟服務和配置干擾

  • 被調用的腳本的輸出,要是stdout,才能被正確寫入influxdb

  • Windows 上文件路徑和符號escape要特別注意

  • 如果對收集性能特別敏感或者收集頻率特別高時,使用Go自定義Plugin

  • 本文內容僅代表個人觀點,與任何公司和組織無關

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

前端也要懂物理 —— 慣性滾動篇

作者:凹凸曼-吖偉

我們在平時編程開發時,除了需要關注技術實現、算法、代碼效率等因素之外,更要把所學到的學科知識(如物理學、理論數學等等)靈活應用,畢竟理論和實踐相輔相成、密不可分,這無論是對於我們的方案選型、還是技術實踐理解都有非常大的幫助。今天就讓我們一起來回顧中學物理知識,並靈活運用到慣性滾動的動效實現當中。

慣性滾動(也叫 滾動回彈momentum-based scrolling)最早是出現在 iOS 系統中,是指 當用戶在終端上滑動頁面然後把手指挪開,頁面不會馬上停下而是繼續保持一定時間的滾動效果,並且滾動的速度和持續時間是與滑動手勢的強烈程度成正比。抽象地理解,就像高速行駛的列車制動后依然會往前行駛一段距離才會最終停下。而且在 iOS 系統中,當頁面滾動到頂/底部時,還有可能觸發 “回彈” 的效果。這裏錄製了微信 APP 【賬單】頁面中的 iOS 原生時間選擇器的慣性滾動效果:

熟悉 CSS 開發的同學或許會知道,在 Safari 瀏覽器中有這樣一條 CSS 規則:

-webkit-overflow-scrolling: touch;

當其樣式值為 touch 時,瀏覽器會使用具有回彈效果的滾動, 即“當手指從觸摸屏上移開,內容會繼續保持一段時間的滾動效果”。除此之外,在豐富多姿的 web 前端生態中,很多經典組件的交互都一定程度地沿用了慣性滾動的效果,譬如下面提到的幾個流行 H5 組件庫中的例子。

流行 UI 庫效果

為了方便對比,我們先來看看一個 H5 普通長列表在 iOS 系統下(開啟了滾動回彈)的滾動表現:

  • weui 的 picker 組件

    明顯可見,weui 選擇器的慣性滾動效果非常弱,基本上手從屏幕上移開后滾動就很快停止了,體驗較為不好。

  • vant 的 picker 組件

    相比之下,vant 選擇器的慣性滾動效果則明顯清晰得多,但是由於觸頂/底回彈時依然維持了普通滾動時的係數或持續時間,導致整體來說回彈的效果有點脫節。

應用物理學模型

慣性 一詞來源於物理學中的慣性定律(即 牛頓第一定律):一切物體在沒有受到力的作用的時候,運動狀態不會發生改變,物體所擁有的這種性質就被稱為慣性。可想而知,慣性滾動的本質就是物理學中的慣性現象,因此,我們可以恰當利用中學物理上的 滑塊模型 來描述慣性滾動全過程。

為了方便描述,我們把瀏覽器慣性滾動效果中的滾動目標(如瀏覽器中的頁面元素)模擬成滑塊模型中的 滑塊。而且分析得出,慣性滾動的全過程可以模擬為(人)使滑塊滑動一定距離然後釋放的過程,那麼,全流程可以拆解為以下兩個階段:

  • 第一階段,滑動滑塊使其從靜止開始做加速運動;

    在此階段,滑塊受到的 F拉 大於 F摩 使其從左到右勻加速前進。

    需要注意的是,對於瀏覽器的慣性滾動來說,我們一般關注的是用戶即將釋放手指前的一小階段,而非滾動的全流程(全流程意義不大),這一瞬間階段可以簡單模擬為滑塊均衡受力做 勻加速運動。

  • 第二階段,釋放滑塊使其在只受摩擦力的作用下繼續滑動,直至最終靜止;

    在此階段,滑塊只受到反向的摩擦力,會維持從左到右的運動方向減速前進然後停下。

基於滑塊模型,我們需要找到適合的量化指標來建立慣性滾動的計算體系。結合模型和具體實現,我們需要關注 滾動距離速度曲線 以及 滾動時長 這幾個關鍵指標,下面會一一展開解析。

滾動距離

對於滑動模型的第一階段,滑塊做勻加速運動,我們不妨設滑塊的滑動距離為 s1,滑動的時間為 t1,結束時的臨界點速度(末速度)為 v1 ,根據位移公式

可以得出速度關係

對於第二階段,滑塊受摩擦力 F拉 做勻減速運動,我們不妨設滑動距離為 s2,滑動的時間為 t2,滑動加速度為 a,另外初速度為 v1,末速度為 0m/s,結合位移公式和加速度公式

可以推算出滑動距離 s2

由於勻減速運動的加速度為負(即 a < 0),不妨設一個加速度常量 A,使其滿足 A = -2a 的關係,那麼滑動距離

然而在瀏覽器實際應用時,v1 算平方會導致最終計算出的慣性滾動距離太大(即對滾動手勢的強度感應過於靈敏),我們不妨把平方運算去掉

所以,求慣性滾動的距離(即 s2)時,我們只需要記錄用戶滾動的 距離 s1滾動時長 t1,並設置一個合適的 加速度常量 A 即可。

經大量測試得出,加速度常量 A 的合適值為 0.003

另外,需要注意的是,對於真正的瀏覽器慣性滾動效果來說,這裏討論的滾動距離和時長是指能夠作用於慣性滾動的範圍內的距離和時長,而非用戶滾動頁面元素的全流程,詳細的可以看【啟停條件】這一節內容。

慣性滾動速度曲線

針對慣性滾動階段,也就是第二階段中的勻減速運動,根據位移公式可以得到位移差和時間間距 T 的關係

不難得出,在同等時間間距條件下,相鄰兩段位移差會越來越小,換句話說就是慣性滾動的偏移量增加速度會越來越小。這與 CSS3 transition-timing-function 中的 ease-out 速度曲線非常吻合,ease-out (即 cubic-bezier(0, 0, .58, 1))的貝塞爾曲線為

曲線圖來自 在線繪製貝塞爾曲線網站。

其中,圖表中的縱坐標是指 動畫推進的進程,橫坐標是指 時間,原點坐標為 (0, 0),終點坐標為 (1, 1),假設動畫持續時間為 2 秒,(1, 1) 坐標點則代表動畫啟動后 2 秒時動畫執行完畢(100%)。根據圖表可以得出,時間越往後動畫進程的推進速度越慢,符合勻減速運動的特性。

我們試試實踐應用 ease-out 速度曲線:

很明顯,這樣的速度曲線過於線性平滑,減速效果不明顯。我們參考 iOS 滾動回彈的效果重複測試,調整貝塞爾曲線的參數為 cubic-bezier(.17, .89, .45, 1)

調整曲線后的效果理想很多:

回彈

接下來模擬慣性滾動時觸碰到容器邊界觸發回彈的情況。

我們基於滑塊模型來模擬這樣的場景:滑塊左端與一根彈簧連接,彈簧另一端固定在牆體上,在滑塊向右滑動的過程中,當滑塊到達臨界點(彈簧即將發生形變時)而速度還沒有降到 0m/s 時,滑塊會繼續滑動並拉動彈簧使其發生形變,同時滑塊會受到彈簧的反拉力作減速運動(動能轉化為內能);當滑塊速度降為 0m/s 時,此時彈簧的形變量最大,由於彈性特質彈簧會恢復原狀(內能轉化成動能),並拉動滑塊反向(左)運動。

類似地,回彈過程也可以分為下面兩個階段:

  • 滑塊拉動彈簧往右做變減速運動;

此階段滑塊受到摩擦力 F摩 和越來越大的彈簧拉力 F彈 共同作用,加速度越來越大,導致速度降為 0m/s 的時間會非常短。

  • 彈簧恢復原狀,拉動滑塊向左做先變加速后變減速運動;

    此階段滑塊受到的摩擦力 F摩 和越來越小的彈簧拉力 F彈 相互抵消,剛開始 F彈 > F摩,滑塊做加速度越來越小的變加速運動;隨後 F彈 < F摩,滑塊做加速度越來越大的變減速運動,直至最終靜止。這裏為了方便實際計算,我們不妨假設一個理想狀態:當滑塊靜止時彈簧剛好恢復形變

回彈距離

根據上面的模型分析,回彈的第一階段做加速度越來越大的變減速直線運動,不妨設此階段的初速度為 v0,末速度為 v1,那麼可以與滑塊位移建立關係:

其中 a 為加速度變量,這裏暫不展開討論。那麼,根據物理學的彈性模型,第二階段的回彈距離為

微積分都來了,簡直沒法計算……

然而,我們可以根據運動模型適當簡化 S回彈 值的計算。由於 回彈第二階段的加速度 是大於 非回彈慣性滾動階段的加速度F彈 + F摩 > F摩)的,不妨設非回彈慣性滾動階段的總距離為 S滑,那麼

因此,我們可以設置一個較為合理的常量 B,使其滿足這樣的等式:

經大量實踐得出,常量 B 的合理值為 10。

回彈速度曲線

觸發回彈的整個慣性滾動軌跡可以拆分成三個運動階段:

然而,如果要把階段 a 和階段 b 準確描繪成 CSS 動畫是有很高的複雜度的:

  • 階段 b 中的變減速運動難以準確描繪;
  • 這兩個階段雖運動方向相同但動畫速度曲線不連貫,很容易造成用戶體驗斷層;

為了簡化流程,我們把階段 ab 合併成一個運動階段,那麼簡化后的軌跡就變成:

鑒於在階段 a 末端的反向加速度會越來越大,所以此階段滑塊的速度驟減同比非回彈慣性滾動更快,對應的貝塞爾曲線末端就會更陡。我們選擇一條較為合理的曲線 cubic-bezier(.25, .46, .45, .94)

對於階段 b,滑塊先變加速后變減速,與 ease-in-out 的曲線有點類似,實踐嘗試:

仔細觀察,我們發現階段 a 和階段 b 的銜接不夠流暢,這是由於 ease-in-out 曲線的前半段緩入導致的。所以,為了突出效果我們選擇只描繪變減速運動的階段 b 末段。貝塞爾曲線調整為 cubic-bezier(.165, .84, .44, 1)

實踐效果:

由於 gif 轉格式導致部分掉幀,示例效果看起來會有點卡頓,建議直接體驗 demo

CSS 動效時長

我們對 iOS 的滾動回彈效果做多次測量,定義出體驗良好的動效時長參數。在一次慣性滾動中,可能會出現下面兩種情況,對應的動效時間也不一樣:

  • 沒有觸發回彈

    慣性滾動的合理持續時間為 2500ms

  • 觸發回彈

    對於階段 a,當 S回彈 大於某個關鍵閾值時定義為 強回彈,動效時長為 400ms;反之則定義為 弱回彈,動效時長為 800ms

    而對於階段 b,反彈的持續時間為 500ms 較為合理。

啟停條件

前文中有提到,如果把用戶滾動頁面元素的整個過程都納入計算範圍是非常不合理的。不難想象,當用戶以非常緩慢的速度使元素滾動比較大的距離,這種情況下元素動量非常小,理應不觸發慣性滾動。因此,慣性滾動的觸發是有條件的。

  • 啟動條件

    慣性滾動的啟動需要有足夠的動量。我們可以簡單地認為,當用戶滾動的距離足夠大(大於 15px)和持續時間足夠短(小於 300ms)時,即可產生慣性滾動。換成編程語言就是,最後一次 touchmove 事件觸發的時間和 touchend 事件觸發的時間間隔小於 300ms,且兩者產生的距離差大於 15px 時認為可啟動慣性滾動。

  • 暫停時機

    當慣性滾動未結束(包括處於回彈過程),用戶再次觸碰滾動元素時,我們應該暫停元素的滾動。在實現原理上,我們需要通過 getComputedStylegetPropertyValue 方法獲取當前的 transform: matrix() 矩陣值,抽離出元素的水平 y 軸偏移量后重新調整 translate 的位置。

示例代碼

基於 vuejs 提供了部分關鍵代碼,也可以直接訪問 codepen demo 體驗效果(完整代碼)。

<html>
  <body>
    <div id="app"></div>
    <template id="tpl">
      <div
        ref="wrapper"
        @touchstart.prevent="onStart"
        @touchmove.prevent="onMove"
        @touchend.prevent="onEnd"
        @touchcancel.prevent="onEnd"
        @transitionend="onTransitionEnd">
        <ul ref="scroller" :style="scrollerStyle">
          <li v-for="item in list">{{item}}</li>
        </ul>
      </div>
    </template>
    <script>
      new Vue({
        el: '#app',
        template: '#tpl',
        computed: {
          list() {},
          scrollerStyle() {
            return {
              'transform': `translate3d(0, ${this.offsetY}px, 0)`,
              'transition-duration': `${this.duration}ms`,
              'transition-timing-function': this.bezier,
            };
          },
        },
        data() {
          return {
            minY: 0,
            maxY: 0,
            wrapperHeight: 0,
            duration: 0,
            bezier: 'linear',
            pointY: 0,                    // touchStart 手勢 y 坐標
            startY: 0,                    // touchStart 元素 y 偏移值
            offsetY: 0,                   // 元素實時 y 偏移值
            startTime: 0,                 // 慣性滑動範圍內的 startTime
            momentumStartY: 0,            // 慣性滑動範圍內的 startY
            momentumTimeThreshold: 300,   // 慣性滑動的啟動 時間閾值
            momentumYThreshold: 15,       // 慣性滑動的啟動 距離閾值
            isStarted: false,             // start鎖
          };
        },
        mounted() {
          this.$nextTick(() => {
            this.wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height;
            this.minY = this.wrapperHeight - this.$refs.scroller.getBoundingClientRect().height;
          });
        },
        methods: {
          onStart(e) {
            const point = e.touches ? e.touches[0] : e;
            this.isStarted = true;
            this.duration = 0;
            this.stop();
            this.pointY = point.pageY;
            this.momentumStartY = this.startY = this.offsetY;
            this.startTime = new Date().getTime();
          },
          onMove(e) {
            if (!this.isStarted) return;
            const point = e.touches ? e.touches[0] : e;
            const deltaY = point.pageY - this.pointY;
            this.offsetY = Math.round(this.startY + deltaY);
            const now = new Date().getTime();
            // 記錄在觸發慣性滑動條件下的偏移值和時間
            if (now - this.startTime > this.momentumTimeThreshold) {
              this.momentumStartY = this.offsetY;
              this.startTime = now;
            }
          },
          onEnd(e) {
            if (!this.isStarted) return;
            this.isStarted = false;
            if (this.isNeedReset()) return;
            const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
            const duration = new Date().getTime() - this.startTime;
            // 啟動慣性滑動
            if (duration < this.momentumTimeThreshold && absDeltaY > this.momentumYThreshold) {
              const momentum = this.momentum(this.offsetY, this.momentumStartY, duration);
              this.offsetY = Math.round(momentum.destination);
              this.duration = momentum.duration;
              this.bezier = momentum.bezier;
            }
          },
          onTransitionEnd() {
            this.isNeedReset();
          },
          momentum(current, start, duration) {
            const durationMap = {
              'noBounce': 2500,
              'weekBounce': 800,
              'strongBounce': 400,
            };
            const bezierMap = {
              'noBounce': 'cubic-bezier(.17, .89, .45, 1)',
              'weekBounce': 'cubic-bezier(.25, .46, .45, .94)',
              'strongBounce': 'cubic-bezier(.25, .46, .45, .94)',
            };
            let type = 'noBounce';
            // 慣性滑動加速度
            const deceleration = 0.003;
            // 回彈阻力
            const bounceRate = 10;
            // 強弱回彈的分割值
            const bounceThreshold = 300;
            // 回彈的最大限度
            const maxOverflowY = this.wrapperHeight / 6;
            let overflowY;

            const distance = current - start;
            const speed = 2 * Math.abs(distance) / duration;
            let destination = current + speed / deceleration * (distance < 0 ? -1 : 1);
            if (destination < this.minY) {
              overflowY = this.minY - destination;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.max(this.minY - maxOverflowY, this.minY - overflowY / bounceRate);
            } else if (destination > this.maxY) {
              overflowY = destination - this.maxY;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.min(this.maxY + maxOverflowY, this.maxY + overflowY / bounceRate);
            }

            return {
              destination,
              duration: durationMap[type],
              bezier: bezierMap[type],
            };
          },
          // 超出邊界時需要重置位置
          isNeedReset() {
            let offsetY;
            if (this.offsetY < this.minY) {
              offsetY = this.minY;
            } else if (this.offsetY > this.maxY) {
              offsetY = this.maxY;
            }
            if (typeof offsetY !== 'undefined') {
              this.offsetY = offsetY;
              this.duration = 500;
              this.bezier = 'cubic-bezier(.165, .84, .44, 1)';
              return true;
            }
            return false;
          },
          // 停止滾動
          stop() {
            const matrix = window.getComputedStyle(this.$refs.scroller).getPropertyValue('transform');
            this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
          },
        },
      });
    </script>
  </body>
</html>

參考資料

  • weui-picker
  • better-scroll

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

【優雅寫代碼系統】springboot+mybatis+pagehelper+mybatisplus+druid教你如何優雅寫代碼

目錄

  • spring基本搭建
  • 整合mybatis
  • pom配置
  • mybatis配置
    • 設置數據源
    • 設置sqlsessionfactory
    • 設置掃描
    • 設置開啟事務
    • 資源放行
    • 測試
    • 結果
  • 思考&&疑問
  • 使用通用mapper功能
  • 使用mybatis-plus
  • 使用分頁插件
    • mybatis-plus自帶分頁
    • github分頁插件
      • 使用常規版本
  • 總結
  • # 加入戰隊
    • 微信公眾號
  • 主題

springboot 融合了很多插件。springboot相比spring來說有一下有點

  • 自動配置: 針對很多spring的應用程序,springboot提供了很多自動配置
  • 起步依賴: 告訴springboot你需要什麼,他就會引入需要的庫
  • 命令行界面:springboot的可選特性
  • Autuator: 監控springboot項目的運行情況

spring基本搭建

  • springboot的配置很簡單。在pom中繼承springboot的pom .然後依賴一下pom就可以繼承所需的jar了

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>


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

  • 出了jar外。我們pom中配置一個插件就行了

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

整合mybatis

【詳見feature/0002/spring-mybatis 分支】

  • 之前我們梳理過mybatis的運行機制及注意點。javaweb的開發時離不開spring的。一個完整的框架是離不開spirng的。所以spring整合mybatis勢在必行。我們下面實現如何在spring下融合mybatis及其優秀的一些插件搭建架構

pom配置

  • 我們在springboot項目的pom中繼續添加我們mybatis的jar就完成了第一步。
  • 一個是mybatis與spring的整合jar 。 開啟springboot加載mybatis項目
  • 一個是spring的aopjar包。主要是實現mybatis的事務問題
  • 一個是mysql的jar包。這裏主要看你自己的需求。如果你的項目中使用oracle那麼久加入oracle的坐標就行了
  • druid是管理數據的數據源

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>

mybatis配置

  • 上面的pom設置已經準備了mybatis開發階段的基本jar。 有了上述的包就可以搭建mybatis . 正常我們在我們的springboot項目中配置一個bean配置的類 MybatisConfig

設置數據源


@Bean
@ConfigurationProperties("spring.datasource")
public DataSource primaryDataSource() {
    // 這裏為了演示,暫時寫死 。 實際上是可以通過autoConfigure裝配參數的
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://192.168.44.130:3306/others?useUnicode=true&amp;characterEncoding=utf8");
    return dataSource;
}

  • 在springboot中我們只需要在方法上添加Bean註解,就相當於我們在spring的xml中配置的bean標籤。在java代碼中我們可以進行我們業務處理。個人理解感覺更加的可控。 因為我們使用的mysql。所以這裏我們簡單的使用druid的數據源

設置sqlsessionfactory


@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/com/github/zxhtom.**./*.xml"));
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 在mybatis中sqlsesson是我們操作數據庫最直接的java類。管理sqlsession就是上面的sqlsessionFactory 。 所以我們配置它也是必須的。因為和spring整合。Mybatis的SqlsessionFactory交由給spring的SqlsessionfactoryBean管理。所以我們先構建SqlSessionFactoryBean。然後配置必須的數據源、Configuration、mybatis的xml路徑等等信息
  • SqlSessionFactoryBean 設置出了spring的FactoryBean屬性意外。最重要的是可以設置Mybatis的sqlsessionfactory的屬性。上面我們只是簡單的設置了。後期可以根據架構的需求進行不斷的完善。
  • 可以設置插件、緩存、別名、映射處理器、事務、環境等等所有mybatis的配置

設置掃描


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • springboot 中我們的mapper接口也是交由spring來掃描的。之前mybatis程序我們是一個一個手動加入的。spring的特性可以直接掃描指定路徑的所有java類。這裏我們就設置了一下mapper的路徑。

設置開啟事務

  • 優秀的架構沒有事務是能使用的。我們配置事務也是很簡單的。在springboot中我們推薦使用java代碼來配置事務的。下面的配置我們為了容易閱讀。配置在TransactionConfig類中

@Bean
public DataSourceTransactionManager primaryTransactionManager() {
    return new DataSourceTransactionManager(dataSource);
}

  • 首先是配置事務管理器。事務管理的是數據源。所以這裏我們需要將MybatisConfig中的DataSource加載進來。這裏不多說
  • 然後配置我們的通知點

@Bean
public TransactionInterceptor txAdvice() {
    DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
    txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
    txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    txAttr_REQUIRED_READONLY.setReadOnly(true);

    NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
    source.addTransactionalMethod("add*", txAttr_REQUIRED);
    source.addTransactionalMethod("save*", txAttr_REQUIRED);
    source.addTransactionalMethod("delete*", txAttr_REQUIRED);
    source.addTransactionalMethod("update*", txAttr_REQUIRED);
    source.addTransactionalMethod("exec*", txAttr_REQUIRED);
    source.addTransactionalMethod("set*", txAttr_REQUIRED);
    source.addTransactionalMethod("get*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("query*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("find*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("list*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("count*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("is*", txAttr_REQUIRED_READONLY);
    return new TransactionInterceptor(primaryTransactionManager(), source);
}

  • 最後我們配置一下我們的切面、切點

@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}

  • 最後說明下。TransactionConfig這個類因為配置事務、攔截所以我們需要加入如下註解在雷傷

@Configuration
@Aspect
@EnableTransactionManagement

資源放行

  • 以上就是mybatis基本的一個配置了。但是這個時候還是不能使用的。因為我們的mapper對應的xml是放在java目錄下的。正常src下都是java。xml文件在maven編譯時不放行的。我們需要特殊處理下

  • 在pom的build下加入放行配置


<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
            <include>**/*.properties</include>
            <include>**/*.yaml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <includes>
            <include>**/*.*</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>

  • 加入如下配置后別忘記了clean一下在運行。防止緩存。clean之後看看target下文件

測試

  • 為了方便測試我們需要編寫測試類。

@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestDataSource {
  
    @Autowired
    private TestMapper testMapper;
    
    @Test
    public void test() {
        List<Map<String, Object>> test = testMapper.test();
        System.out.println(test);
    }
}

結果

  • 這裡是我查詢數據庫中的一條數據。主要是測試springboot整合mybatis的流程。到這裏mybatis就整合完了。

思考&&疑問

  • 上面的配置我們是接入mybatis了。但是在筆者其他架構上測試過。在MybatisConfig這個類中配置數據源的時候可以直接new出DruidDataSource就可以了。springboot會自動通過DataSourceProperties這個類獲取到數據源信息的。但是筆者這裏一直沒有嘗試成功。至於原理更是沒有搞懂了。這裏留下一個彩蛋希望知道的朋友能說說這事怎麼回事

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource primaryDataSource() {
    return new DruidDataSource();
}

  • 上述代碼和本案例中相比就少了很多配置。雖然有的朋友說可以通過映射實體來做。但是直接new對象對我們而言更簡單。疑問

使用通用mapper功能

【詳見feature/0002/spring-mybatis-tk-page 分支】


<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>3.3.9</version>
</dependency>

  • 這些插件其實就是改寫我們之前學習的myabtis四大組件中的某一個組件而實現的。所以不管是通用mapper還是後面要說的myabtis-plus都是需要重新改寫我們的myabtis配置類的。

  • 首先我們想接入通用mapper時,我們需要改用tk提供的掃包配置


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    tk.mybatis.spring.mapper.MapperScannerConfigurer mapperScannerConfigurer = new tk.mybatis.spring.mapper.MapperScannerConfigurer();
    //MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • 然後添加通用mapper配置

@Bean()
public MapperHelper primaryMapperHelper() throws Exception {
    MapperHelper mapperHelper = new MapperHelper();

    Config config = mapperHelper.getConfig();
    //表名字段名使用駝峰轉下劃線大寫形式
    config.setStyle(Style.camelhumpAndUppercase);

    mapperHelper.registerMapper(Mapper.class);
    mapperHelper.processConfiguration(primarySqlSessionFactory().getConfiguration());
    return mapperHelper;
}

  • 測試代碼就很簡單了。

User user = tkMapper.selectByPrimaryKey(1);

使用mybatis-plus

【詳見feature/0002/spring-mybatisplus 分支】

  • mybatis-plus 實際上就是通用mapper 。這裏可以理解就是不同的實現。接入mybatis-plus其實很簡單。首先我們引入坐標


<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

  • 然後我們創建實體

@Data
@TableName(value = "person_info_large")
public class User {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String account;
    private String name;
    private String area;
    private String title;
    private String motto;
}

  • 然後就是編寫我們的Mapper 。需要繼承BaseMapper

public interface PlusMapper extends BaseMapper<User> {
}

  • 上述的編寫之後基本就可以查詢了。但是注意一下我們需要修改上面的myabtisCOnfog這個類。因為接入mybatisplus后需要我們用mybatisplus中的sqlsessionbean。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 和通用mapper一樣。就這樣我們就可以調用BaseMapper為我們提供的方法操作數據庫了。

@Test
public void plusTest() {
    User user = plusMapper.selectById(1);
    System.out.println(user);
}

使用分頁插件

mybatis-plus自帶分頁

【詳見feature/0002/spring-mybatisplus 分支】

  • 首先我們使用mybatis plus自帶的分頁插件,將插件類注入到容器中

@Bean
public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
}

  • 然後我們查詢的構造一個Page對象就行了。參數分別是第幾頁、展示條數

return plusMapper.selectPage(page, null);

  • 就是這麼簡單。使用簡單原理才是我們學習的重點。喜歡myabtis插件原理的可以留言。抽空可以研究

github分頁插件

【詳見feature/0002/spring-mybatis-tk-page 分支】

  • 因為myatis-plus自帶了分頁插件。上面也展示了如何使用myabtis-plus插件。還有一個github上開元的分頁插件。這裏就和通用mapper進行組合使用

  • pagehelper官方更新還是挺勤奮的。提供了pagehelper 和springboot auto裝配兩種。 筆者這裏測試了pagehelper-spring-boot-starter這個包不適合本案例中。因為本案例中掃描mapper路徑是通過MapperScannerConfigurer註冊的。如果使用pagehelper-spring-boot-starter的話就會導致分頁攔截器失效。我們看看源碼

  • 這是因為這版本提供了springboot自動裝配。但是自動裝配的代碼中進行添加攔截器的時候sqlsessionfactory這個時候還沒有進行掃描mapper.也就沒有進行addMapper 。 所以這個時候添加的攔截器攔截不到我們的mapper . 如果非要使用這個版本的話。我們掃描mapper就不能通過MapperScannerConfigurer . 經過筆者測試。需要在MybatisConfig類上添加掃描註解@MapperScan(basePackages = {"com.github.zxhtom.**.mapper"}, sqlSessionFactoryRef = "primarySqlSessionFactory")

使用常規版本

  • 為了符合本版本宗旨 。 我們這裏使用如下坐標

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.7</version>
</dependency>

  • 使用這個版本pagehelper。 沒有了springboot的自動裝配了。我們可以自己添加插件。添加插件有兩種方式。想自動裝配版本一樣通過Configuration進行添加不過我們通過Condition條件選擇添加的時機 。
  • 還有一個簡單的方式就是通過SqlSessionFactoryBean設置sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
  • 因為通過sqlsessionFactoryBean添加的插件和在settings文件中添加是一樣的。在通過XmlFactory.build Configuration對象是會自動將插件裝在上。這個時候mapper也都掃描過了。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

總結

  • 這裏簡單闡述下為什麼不適用註解掃描mapper、或者不在配置文件中配置掃包路徑。因為通過MapperScannerConfigurer我們可以動態控制路徑。這樣顯得比較有逼格。在實際開發中筆者推薦使用註解方式掃描。因為這樣可以避免不必要的坑。

  • 到這裏我們整理了【springboot整合mybaits】、【mybatis-plus】、【通用mapper】、【pagehelper插件整合】 這四個模塊的整理中我們發現離不開myabtis的四大組件。springboot其實我們還未接觸到深的內容。這篇文章主要偏向myabtis . 後續還會繼續衍生探索 【myabtis+druid】監控數據信息 。

  • 喜歡研究源碼和開元框架小試牛刀的歡迎關注我

加入戰隊

# 加入戰隊

微信公眾號

主題

非xml配置springboot+mybatis事務管理

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了博客的主題切換,菜單和二維碼的显示與隱藏功能,本篇繼續完成分頁查詢文章列表的數據展示。

添加頁面

現在點擊頁面上的鏈接,都會提示錯誤消息,因為沒有找到對應的路由地址。先在Pages下創建五個文件夾:Posts、Categories、Tags、Apps、FriendLinks。

然後在對應的文件夾下添加Razor組件。

  • Posts文件夾:文章列表頁面Posts.razor、根據分類查詢文章列表頁面Posts.Category.razor、根據標籤查詢文章列表頁面Posts.Tag.razor、文章詳情頁Post.razor
  • Categories文件夾:分類列表頁面Categories.razor
  • Tags文件夾:標籤列表頁面Tags.razor
  • Apps文件夾:Apps.razor準備將友情鏈接入口放在裏面
  • FriendLinks文件夾:友情鏈接列表頁面FriendLinks.razor

先分別創建上面這些Razor組件,差不多除了後台CURD的頁面就這些了,現在來逐個突破。

不管三七二十一,先把所有頁面的路由給確定了,指定頁面路由使用 @page 指令,官方文檔說不支持可選參數,但是可以支持多個路由規則。

默認先什麼都不显示,可以將之前的加載中圈圈寫成一個組件,供每個頁面使用。

在Shared文件夾添加組件Loading.razor

<!--Loading.razor-->
<div class="loader"></div>
//Posts.razor
@page "/posts/"
@page "/posts/page/{page:int}"
@page "/posts/{page:int}"

<Loading />

@code {
    /// <summary>
    /// 當前頁碼
    /// </summary>
    [Parameter]
    public int? page { get; set; }
}

這裏我加了三條,可以匹配沒有page參數,帶page參數的,/posts/page/{page:int}這個大家可以不用加,我是用來兼容目前線上的博客路由的。總的來說可以匹配到:/posts/posts/1/posts/page/1這樣的路由。

//Posts.Category.razor
@page "/category/{name}"

<Loading />

@code {
    /// <summary>
    /// 分類名稱參數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

根據分類名稱查詢文章列表頁面,name當作分類名稱參數,可以匹配到類似於:/category/aaa/category/bbb這樣的路由。

//Posts.Tag.razor
@page "/tag/{name}"

<Loading />

@code {
    /// <summary>
    /// 標籤名稱參數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

這個根據標籤名稱查詢文章列表頁面和上面差不多一樣,可以匹配到:/tag/aaa/tag/bbb這樣的路由。

//Post.razor
@page "/post/{year:int}/{month:int}/{day:int}/{name}"

<Loading />

@code {
    [Parameter]
    public int year { get; set; }

    [Parameter]
    public int month { get; set; }

    [Parameter]
    public int day { get; set; }

    [Parameter]
    public string name { get; set; }
}

文章詳情頁面的路由有點點複雜,以/post/開頭,加上年月日和當前文章的語義化名稱組成。分別添加了四個參數年月日和名稱,用來接收URL的規則,使用int來設置路由的約束,最終可以匹配到路由:/post/2020/06/09/aaa/post/2020/06/9/bbb這樣的。

//Categories.razor
@page "/categories"

<Loading />

//Tags.razor
@page "/tags"

<Loading />

//FriendLinks.razor
@page "/friendlinks"

<Loading />

分類、標籤、友情鏈接都是固定的路由,像上面這樣就不多說了,然後還剩一個Apps.razor

//Apps.razor
@page "/apps"

<div class="container">
    <div class="post-wrap">
        <h2 class="post-title">-&nbsp;Apps&nbsp;-</h2>
        <ul>
            <li>
                <a target="_blank" href="https://support.qq.com/products/75616"><h3>吐個槽_留言板</h3></a>
            </li>
            <li>
                <NavLink href="/friendlinks"><h3>友情鏈接</h3></NavLink>
            </li>
        </ul>
    </div>
</div>

在裏面添加了一個友情鏈接的入口,和一個 騰訊兔小巢 的鏈接,歡迎大家吐槽留言噢。

現在可以運行一下看看,點擊所有的鏈接都不會提示錯誤,只要路由匹配正確就會出現加載中的圈圈了。

文章列表

在做文章列表的數據綁定的時候遇到了大坑,有前端開發經驗的都知道,JavaScript弱類型語言中接收json數據隨便玩,但是在Blazor中我試了下動態接受傳遞過來的JSON數據,一直報錯壓根運行不起來。所以在請求api接收數據的時候需要指定接收對象,那就好辦了我就直接引用API中的.Application.Contracts就行了啊,但是緊接着坑又來了,目標框架對不上,引用之後也運行不起來,這裏應該是之前沒有設計好。

於是,我就想了一個折中的辦法吧,將API中的返回對象可以用到的DTO先手動拷貝一份到Blazor項目中,後續可以考慮將公共的返回模型做成Nuget包,方便使用。

那麼,最終就是在Blazor中添加一個Response文件夾,用來放接收對象,裏面的內容看圖:

有點傻,先這樣解決,後面在做進一步的優化吧。

將我們複製進來的東東,在_Imports.razor中添加引用。

//_Imports.razor
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Meowv.Blog.BlazorApp.Shared
@using Response.Base
@using Response.Blog

@inject HttpClient Http
@inject Commons.Common Common

@inject HttpClient Http:注入HttpClient,用它來請求API數據。

現在有了接收對象,接下來就好辦了,來實現分頁查詢文章列表吧。

先添加三個私有變量,限制條數,就是一次加載文章的數量,總頁碼用來計算分頁,還有就是API的返回數據的接收類型參數。

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostDto>> posts;

然後當頁面初始化的時候,去加載數據,渲染頁面,因為page參數可能存在為空的情況,所以要考慮進去,當為空的時候給他一個默認值1。

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostDto>>>($"/blog/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化方法中設置默認值,調用RenderPage(...)獲取到API返回來的數據,並根據返回數據計算出頁碼,這樣就可以綁定數據了。

@if (posts == null)
{
    <Loading />
}
else
{
    <div class="post-wrap archive">
        @if (posts.Success && posts.Result.Item.Any())
        {
            @foreach (var item in posts.Result.Item)
            {
                <h3>@item.Year</h3>
                @foreach (var post in item.Posts)
                {
                    <article class="archive-item">
                        <NavLink href="@("/post" + post.Url)">@post.Title</NavLink>
                        <span class="archive-item-date">@post.CreationTime</span>
                    </article>
                }
            }
            <nav class="pagination">
                @for (int i = 1; i <= TotalPage; i++)
                {
                    var _page = i;

                    if (page == _page)
                    {
                        <span class="page-number current">@_page</span>
                    }
                    else
                    {
                        <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/posts/@_page">@_page</a>
                    }
                }
            </nav>
        }
        else
        {
            <ErrorTip />
        }
    </div>
}

在加載數據的時候肯定是需要一個等待時間的,因為不可抗拒的原因數據還沒加載出來的時候,可以讓它先轉一會圈圈,當posts不為空的時候,再去綁定數據。

在綁定數據,for循環頁碼的時候我又遇到了一個坑,這裏不能直接去使用變量i,必須新建一個變量去接受它,不然我傳遞給RenderPage(...)的參數就會是錯的,始終會取到最後一次循環的i值。

當判斷數據出錯或者沒有數據的時候,在把錯誤提示<ErrorTip />扔出來显示。

做到這裏,可以去運行看看了,肯定會報錯,因為還有一個重要的東西沒有改,就是我們接口的BaseAddress,在Program.cs中,默認是當前Blazor項目的運行地址。

我們需要先將API項目運行起來,拿到地址配置在Program.cs中,因為現在還是本地開發,有多種辦法可以解決,可以將.HttpApi.Hosting設為啟動項目直接運行起來,也可以使用命令直接dotnet run

我這裏為了方便,直接發布在IIS中,後續只要電腦打開就可以訪問了,你甚至選擇其它任何你能想到的方式。

關於如何發布這裏先不做展開,有機會的話寫一篇將.net core開發的項目發布到 Windows、Linux、Docker 的教程吧。

所以我的Program.cs中配置如下:

//Program.cs
using Meowv.Blog.BlazorApp.Commons;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            var baseAddress = "https://localhost";

            if (builder.HostEnvironment.IsProduction())
                baseAddress = "https://api.meowv.com";

            builder.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri(baseAddress)
            });

            builder.Services.AddSingleton(typeof(Common));

            await builder.Build().RunAsync();
        }
    }
}

baseAddress默認為本地開發地址,使用builder.HostEnvironment.IsProduction()判斷是否為線上正式生產環境,改變baseAddress地址。

現在可以看到已經可以正常獲取數據,並且翻頁也是OK的,然後又出現了一個新的BUG。

解決BUG

細心的可以發現,當我點擊頭部組件的Postsa 標籤菜單時候,頁面沒有發生變化,只是路由改變了。

思來想去,我決定使用NavigationManager這個URI和導航狀態幫助程序來解決,當點擊頭部的Postsa 標籤菜單直接刷新頁面得了。

Common.cs中使用構造函數注入NavigationManager,然後添加一個跳轉指定URL的方法。

/// <summary>
/// 跳轉指定URL
/// </summary>
/// <param name="uri"></param>
/// <param name="forceLoad">true,繞過路由刷新頁面</param>
/// <returns></returns>
public async Task RenderPage(string url, bool forceLoad = true)
{
    _navigationManager.NavigateTo(url, forceLoad);

    await Task.CompletedTask;
}

forceLoad = true的時候,將會繞過路由直接強制刷新頁面,如果forceLoad = false,則不會刷新頁面。

緊接着在Header.razor中修改代碼,添加點擊事件。

@*<NavLink class="menu-item" href="posts">Posts</NavLink>*@

<NavLink class="menu-item" href="posts" @onclick="@(async () => await Common.RenderPage("posts"))">Posts</NavLink>

總算是搞定,完成了分頁查詢文章列表的數據綁定,今天就到這裏吧,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!