標準庫bufio個人詳解

本文是我有通俗的語言寫的如果有誤請指出。

先看bufio官方文檔

https://studygolang.com/pkgdoc文檔地址

 

 主要分三部分Reader、Writer、Scanner

分別是讀數據、寫數據和掃描器三種數據類型的相關操作 這個掃描後面會詳細說我開始也沒弄明白其實很簡單。

 

Reader

func 

func NewReaderSize(rd ., size ) *

NewReaderSize創建一個具有最少有size尺寸的緩衝、從r讀取的*Reader。如果參數r已經是一個具有足夠大緩衝的* Reader類型值,會返回r。

 

 

 解釋:看官方解釋這個方法可能不太容易懂,這個意思就是就是你可以給*Reader自定義一個size大小的緩衝區,*Reader每次從底層io.Reader(也就是你那個文件或者流)中預讀size大小的數據到緩衝區中(可能讀不滿),然後你每次讀數據實際是從這個緩衝區中拿數據。

 

 下面是NewReaderSize源碼

func NewReaderSize(rd io.Reader, size int) *Reader {
    // Is it already a Reader?
    b, ok := rd.(*Reader)
    if ok && len(b.buf) >= size {
        return b
    }
    if size < minReadBufferSize { //minReadBufferSize==16
        size = minReadBufferSize
    }
    r := new(Reader)
    r.reset(make([]byte, size), rd)
    return r
}

  r.reset 初始化了一個*Reader 返回大小是size。

func 

func NewReader(rd .) *

NewReader創建一個具有默認大小緩衝、從r讀取的*Reader。

解釋:那這個NewReader就很好解釋了 和NewReaderSize基本一樣就是緩衝區大小是默認設置好的

func (*Reader) 

func (b *) Peek(n ) ([], )

解釋:Peek就是返回緩存的一個切片,該切片引用緩存中的前N個字節的數據,如果n大於總大小,則返回能讀到的字節數的數據。

func (*Reader) 

func (b *) Read(p []) (n , err )

Read讀取數據寫入p。本方法返回寫入p的字節數。本方法一次調用最多會調用下層Reader接口一次Read方法,因此返回值n可能小於len(p)。讀取到達結尾時,返回值n將為0而err將為io.EOF。

解釋:如果緩存不為空則直接從緩存中讀數據不會從底層io.Reader讀,如果緩存為空len(p)>緩存大小,則直接從底層io.Reader讀數據到p。

如果len(p)<緩存大小,則先從底層io.Reader中讀數據到緩存再到p。

 

主要就這幾個 還有幾個文檔寫的都很清楚易懂我就不多寫了。

Writer類型的方法和Reader類型的方法差不多也很易懂主要就一個Flush要注意。

func (*Writer) 

func (b *) Flush() 

Flush方法將緩衝中的數據寫入下層的io.Writer接口。

和Reader是倒過來的,Writer每次寫數據是先寫入緩衝區的,進程緩衝區填滿后,通過進程緩衝寫入到內核緩衝再寫入到磁盤,使用Flush就不等填滿直接走寫入流程了,保證你的數據及時寫入文件。

 

 

 

 解釋:scanner類型掃描器 官方的說法很複雜,我也沒太看懂找了很多資料,其實就是你在數據傳輸的時候時候使用“分隔符”,scanner類型可以通過分隔符逐個迭代你的數據。

上面4個函數func Scan……  就是分隔符的判斷函數這4個是給你預設好的,你也可以按照自己的需求改寫。

怎麼改寫呢,看下面

func (*Scanner) 

func (s *) Split(split )

這個Split方法就是設置你這個scanner的用哪個SplitFunc類型的函數

在看下面這個SpliFunc類型的函數簽名

type SplitFunc func(data [], atEOF ) (advance , token [], err )

照着這個格式寫一個不就得了么,當然具體寫法給出了但是你不會?沒關係咱看一下官方是咋寫的。

https://github.com/golang/go/blob/master/src/bufio/scan.go?name=release#57官方源碼地址

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, dropCR(data[0:i]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), dropCR(data), nil
	}
	// Request more data.
	return 0, nil, nil
}

   

看bytes.IndexByte(data, ‘\n’);這段不就是在找行尾嘛 比如你想改成以“;”為分隔符的就改成bytes.IndexByte(data, ‘;’);不就得了么

func main(){
    scanner:=bufio.NewScanner(
        strings.NewReader("abcdefg\nhigklmn"),
    )
    scanner.Split(ScanLines) //這裏可以隨意選擇用哪個函數也可以自定義,可以不指定默認為\n做分隔符
  for scanner.Scan(){
    fmt.Println(scanner.Text())
  }
}

  

到此為止拉~

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

Android DecorView 與 Activity 綁定原理分析

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

概論

Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

DecorView 的創建 

先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

    private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

 private ViewGroup createSubDecor() {
        // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、確保優先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根據不同的設置來對 subDecor 進行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 將 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

3、最後添加到 DecorView中。

添加的具體代碼如下:此處是通過調用 

 // AppCompatDelegateImpl   this.mWindow.getDecorView();

 // phoneWindow    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
 // 生成 DecorView             mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
 // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

 WindowManager

View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代碼主要做了以下幾件事:

1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

 ViewRootImpl

實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

   /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這裏主要關注兩點:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

  void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

Java虛擬機詳解(十)——類加載過程

  在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

1、類的生命周期

  類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

  

  上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

2、加載

  “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

  ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

  ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

  ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

  PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

  另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

  1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

  2、從網絡中獲取。比較典型的應用就是 Applet。

  3、運行時計算生成。這就是動態代理技術。

  4、由其它文件生成。比如 JSP 應用。

  5、從數據庫中讀取。

  加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

  注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

3、驗證

  驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

  所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

①、文件格式驗證

  校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

  一、是否以魔數 0xCAFEBABE 開頭。

  二、主、次版本號是否是當前虛擬機處理範圍之內。

  三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

  四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

  五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

  六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

  以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

②、元數據驗證

  第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

  一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

  二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

  三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

  四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

③、字節碼驗證

  第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

  一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

  二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

  三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

④、符號引用驗證

  符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

  一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

  二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

  三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

4、準備

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

  注意:

  一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

  二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

  

 

   比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

5、解析

  解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

  符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

  直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

  解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

6、初始化

   初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

  在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

  換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程

  ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

  比如如下代碼會報錯:

  

 

   但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

  ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

  ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

  ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

  ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

  ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

  ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

  比如對於如下代碼:

package com.yb.carton.controller;

/**
 * Create by YSOcean
 */
public class ClassLoadInitTest {


    static class Hello{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName() + "init");
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h1 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h2 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();
    }

}

View Code

  運行結果如下:

  

 

   線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

  知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

  ①、當虛擬機啟動時,初始化用戶指定的類。

  ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

  ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

  ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

  ⑤、子類的初始化會觸發父類的初始化。

  ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

  ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

  ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

08 決策樹與隨機森林

08 決策樹與隨機森林

決策樹之信息論基礎

認識決策樹

  1. 來源: 決策樹的思想來源非常樸素,程序設計中的條件分支結構就是if – then 結構,最早的決策樹就是利用這類結構分割數據的一種分類學習方法。

  2. 舉例:是否見相親對象

信息的度量和作用

  1. 克勞德 .艾爾伍德 .香農:信息論創始人,密西根大學學士,麻省理工學院博士。 1948年發表了划時代論文 – 通信的數學原理,奠定了現代信息論的基礎。
  2. 信息的單位: 比特 (bit)

  3. 舉例: 以32支球隊爭奪世界杯冠軍
  • 如果不知道任何球隊的信息,每支球隊得冠概率相等。
    以二分法預測,最少需要使用5次才能預測到準確結果。 5 = log32 (以2為底)
    5 = -(1/32log1/32 + 1/32log1/32 + ……)

  • 開放一些信息,則小於5bit, 如1/6 德國,1/6 巴西, 1/10 中國
    5 > -(1/6log1/4 + 1/6log1/4 + ….)

  1. 信息熵:
  • “誰是世界杯冠軍”的信息量應該比5 bit少, 它的準確信息量應該是:
  • H = -(p1logp1 + p2logp2 + p3logp3 +……p32logp32 ) Pi 為第i支球隊獲勝的概率
  • H 的專業術語就是信息熵,單位為比特

決策樹的劃分以及案例

信息增益

  1. 定義: 特徵A對訓練數據集D的信息增益g(D,A), 定義為集合D的信息熵H(D)與特徵A給定條件下D的信息條件熵H(D|A) 之差,即:
    g(D,A) = H(D) – H(D | A)
    注: 信息增益表示得知特徵 X 的信息而使得類 Y的信息的不確定性減少的程度。

  2. 以不同特徵下的信貸成功率為例

  • H(D) = -(9/15log(9/15) + 6/15log(6/15)) = 0.971 # 以類別進行判斷,只有是否兩種類別
  • gD,年紀) = H(D) – H(D’|年紀) = 0.971 – [1/3H(青年)+ 1/3H(中年)+ 1/3H(老年)] # 三種年紀對應的目標值均佔1/3
    – H(青年) = -(2/5log(2/5) + 3/5log(3/5)) # 青年類型中,類別的目標值特徵為(2/5, 3/5)
    – H(中年) = -(2/5log(2/5) + 3/5log(3/5))
    – H(老年) = -(4/5log(2/5) + 1/5log(3/5))

令A1, A2, A3, A4 分別表示年齡,有工作,有房子和信貸情況4個特徵,則對應的信息增益為:
g(D,A1) = H(D) – H(D|A1)
其中,g(D,A2) = 0.324 , g(D,A3) = 0.420 , g(D,A4) = 0.363
相比而言,A3特徵(有房子)的信息增益最大,為最有用特徵。
所以決策樹的實際劃分為:

常見決策樹使用的算法

  1. ID3
  • 信息增益,最大原則
  1. C4.5
  • 信息增益比最大原則 (信息增益占原始信息量的比值)
  1. CART
  • 回歸樹: 平方誤差最小
  • 分類樹: 基尼係數最小原則 (劃分的細緻),sklearn默認的劃分原則

Sklearn決策樹API

  1. sklearn.tree.DecisionTreeClassifier(criterion=’gini’, max_depth=None, random_state=None)
  • criterion (標準): 默認基尼係數,也可以選用信息增益的熵‘entropy’
  • max_depth: 樹的深度大小
  • random_state: 隨機數種子
  1. 決策樹結構
    sklearn.tree.export_graphviz() 導出DOT文件格式
  • estimator: 估算器
  • out_file = “tree.dot” 導出路徑
  • feature_name = [,] 決策樹特徵名

決策樹預測泰坦尼克號案例

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_graphviz

"""
泰坦尼克數據描述事故后乘客的生存狀態,該數據集包括了很多自建旅客名單,提取的數據集中的特徵包括:
票的類別,存貨,等級,年齡,登錄,目的地,房間,票,船,性別。
乘坐等級(1,2,3)是社會經濟階層的代表,其中age數據存在缺失。
"""


def decision():
    """
    決策樹對泰坦尼克號進行預測生死
    :return: None
    """
    # 1.獲取數據
    titan = pd.read_csv('./titanic_train.csv')

    # 2.處理數據,找出特徵值和目標值
    x = titan[['Pclass', 'Age', 'Sex']]
    y = titan[['Survived']]
    # print(x)

    # 缺失值處理 (使用平均值填充)
    x['Age'].fillna(x['Age'].mean(), inplace=True)
    print(x)
    # 3.分割數據集到訓練集和測試集
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

    # 4. 進行處理(特徵工程) 特徵,類別 --> one_hot編碼
    dict = DictVectorizer(sparse=False)
    x_train = dict.fit_transform(x_train.to_dict(orient='records'))
    print(dict.get_feature_names())
    x_test = dict.transform(x_test.to_dict(orient='records'))  # 默認一行一行轉換成字典
    print(x_train)

    # 5. 用決策樹進行預測
    dec = DecisionTreeClassifier()
    dec.fit(x_train, y_train)

    # 預測準確率
    print("預測的準確率:", dec.score(x_test, y_test))

    # 導出決策樹
    export_graphviz(dec, out_file='./tree.dot', feature_names=['Pclass', 'Age', 'Sex'])
    return None


if __name__ == '__main__':
    decision()

隨機森林

集成學習方法

  1. 定義:集成學習通過建立幾個模型組合,來解決單一預測問題。其工作原理是生成多個分類器 / 模型,各組獨立地學習和作出預測。這些預測最後結合成單預測,因此優於任何一個單分類的租出預測。

隨機森林

  1. 定義:在機器學習中,隨機森林是一個包含多個決策樹的分類器,並且其輸出的類別是由個別樹輸出的類別的眾數而定。
    例如: 訓練了5棵樹,其中4棵樹的結果是True, 1棵樹為False, 那麼最終的結果就是True. (投票)

  2. 問題: 如果每棵樹使用相同的特徵,相同的分類器,參數也相同,建立的每棵樹不就是相同的么?

隨機森林建立多個決策樹的過程:

單個樹的建立:(N個樣本,M個特徵)

  1. 隨機在N個樣本中選擇一個樣本,重複N次, 樣本有可能重複
  2. 隨機在M個特徵當中選出m個特徵 m << M
  3. 建立10棵決策樹,樣本,特徵大多不一樣 隨機有放回的抽樣 (bootstrap抽樣)

為什麼要隨機抽樣訓練集?

如果不隨機抽樣,每棵樹的訓練集都一樣,那麼最終訓練處的樹分類結果也是完全一樣的

為什麼要有放回的抽樣?

如果不是有放回的抽樣,那麼每棵樹的訓練樣本都是不同的,都是沒有交集的,這樣的每棵樹都是“有偏的”,“片面的”。即,每棵樹訓練出來都是有很大的差異,而隨機森鈴最後分類取決於多棵樹(弱分類器)的投票表決。

隨機森林 API

  • 分類器:sklearn.ensemble.RandomForestClassifier
    • n_estimators:integer(整數),option, default=10 (森林里數目的數量)
    • criteria: string (default =’gini’) 分割特徵的測量方法
    • max_depth 樹的最大深度
    • max_feature = ‘auto’ 每個決策樹的最大特徵數量
    • bootstrap: default = True 是否放回抽樣

隨機森林的優點

  1. 在當前的所有算法中,具有極好的準確率
  2. 能有有效地運行在大數據集上 (樣本,特徵)
  3. 能夠處理具有高維特徵的輸入樣本,不需要降維
  4. 能夠評估各個特徵在分類問題上的重要性

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

羞,Spring Bean 初始化/銷毀竟然有這麼多姿勢

文章來源:

一、前言

日常開發過程有時需要在應用啟動之後加載某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命周期,可以在 Bean 創建過程初始化資源,以及銷毀 Bean 過程釋放資源。Spring 提供多種不同的方式初始化/銷毀 Bean,如果同時使用這幾種方式,Spring 如何處理這幾者之間的順序?

有沒有覺得標題很熟悉,沒錯標題模仿二哥 「@沉默王二」 文章。

二、姿勢剖析

首先我們先來回顧一下 Spring 初始化/銷毀 Bean 幾種方式,分別為:

  • init-method/destroy-method
  • InitializingBean/DisposableBean
  • @PostConstruct/@PreDestroy
  • ContextStartedEvent/ContextClosedEvent

PS: 其實還有一種方式,就是繼承 Spring Lifecycle 接口。不過這種方式比較繁瑣,這裏就不再分析。

2.1、init-method/destroy-method

這種方式在配置文件文件指定初始化/銷毀方法。XML 配置如下

<bean id="demoService" class="com.dubbo.example.provider.DemoServiceImpl"  destroy-method="close"  init-method="initMethod"/>

或者也可以使用註解方式配置:

@Configurable
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public HelloService hello() {
        return new HelloService();
    }
}

還記得剛開始接觸學習 Spring 框架,使用就是這種方式。

2.2、InitializingBean/DisposableBean

這種方式需要繼承 Spring 接口 InitializingBean/DisposableBean,其中 InitializingBean 用於初始化動作,而 DisposableBean 用於銷毀之前清理動作。使用方式如下:

@Service
public class HelloService implements InitializingBean, DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("hello destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello init....");
    }
}

2.3、@PostConstruct/@PreDestroy

這種方式相對於上面兩種方式來說,使用方式最簡單,只需要在相應的方法上使用註解即可。使用方式如下:

@Service
public class HelloService {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }
}

這裏踩過一個坑,如果使用 JDK9 之後版本 ,@PostConstruct/@PreDestroy 需要使用 maven 單獨引入 javax.annotation-api,否者註解不會生效。

2.4、ContextStartedEvent/ContextClosedEvent

這種方式使用 Spring 事件機制,日常業務開發比較少見,常用與框架集成中。Spring 啟動之後將會發送 ContextStartedEvent 事件,而關閉之前將會發送 ContextClosedEvent 事件。我們需要繼承 Spring ApplicationListener 才能監聽以上兩種事件。

@Service
public class HelloListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ContextClosedEvent){
            System.out.println("hello ContextClosedEvent");
        }else if(event instanceof ContextStartedEvent){
            System.out.println("hello ContextStartedEvent");
        }

    }
}

也可以使用 @EventListener註解,使用方式如下:

public class HelloListenerV2 {
    
    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("hello ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }
}

PS:只有調用 ApplicationContext#start 才會發送 ContextStartedEvent。若不想這麼麻煩,可以監聽 ContextRefreshedEvent 事件代替。一旦 Spring 容器初始化完成,就會發送 ContextRefreshedEvent

三、綜合使用

回顧完上面幾種方式,這裏我們綜合使用上面的四種方式,來看下 Spring 內部的處理順序。在看結果之前,各位讀者大人可以猜測下這幾種方式的執行順序。

public class HelloService implements InitializingBean, DisposableBean {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bye DisposableBean...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello InitializingBean....");
    }

    public void xmlinit(){
        System.out.println("hello xml-init...");
    }

    public void xmlDestory(){
        System.out.println("bye xmlDestory...");
    }

    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("bye ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }

}

xml 配置方式如下:

    <context:annotation-config />
    <context:component-scan base-package="com.dubbo.example.demo"/>
    
    <bean class="com.dubbo.example.demo.HelloService" init-method="xmlinit" destroy-method="xmlDestory"/>

應用啟動方法如下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
context.close();

程序輸出結果如下所示:

最後採用圖示說明總結以上結果:

四、源碼解析

不知道各位讀者有沒有猜對這幾種方式的執行順序,下面我們就從源碼角度解析 Spring 內部處理的順序。

4.1、初始化過程

使用 ClassPathXmlApplicationContext 啟動 Spring 容器,將會調用 refresh 方法初始化容器。初始化過程將會創建 Bean 。最後當一切準備完畢,將會發送 ContextRefreshedEvent。當容器初始化完畢,調用 context.start() 就發送 ContextStartedEvent 事件。

refresh 方法源碼如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
            //... 忽略無關代碼

            // 初始化所有非延遲初始化的 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 發送 ContextRefreshedEvent
            finishRefresh();

            //... 忽略無關代碼
    }
}

一路跟蹤 finishBeanFactoryInitialization 源碼,直到 AbstractAutowireCapableBeanFactory#initializeBean,源碼如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 調用 BeanPostProcessor#postProcessBeforeInitialization 方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 初始化 Bean
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
}

BeanPostProcessor 將會起着攔截器的作用,一旦 Bean 符合條件,將會執行一些處理。這裏帶有 @PostConstruct 註解的 Bean 都將會被 CommonAnnotationBeanPostProcessor 類攔截,內部將會觸發 @PostConstruct 標註的方法。

接着執行 invokeInitMethods ,方法如下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
        throws Throwable {

    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        // 省略無關代碼
        // 如果是 Bean 繼承 InitializingBean,將會執行  afterPropertiesSet 方法
        ((InitializingBean) bean).afterPropertiesSet();
    }

    if (mbd != null) {
        String initMethodName = mbd.getInitMethodName();
        if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 執行 XML 定義 init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

如果 Bean 繼承 InitializingBean 接口,將會執行 afterPropertiesSet 方法,另外如果在 XML 中指定了 init-method ,也將會觸發。

上面源碼其實都是圍繞着 Bean 創建的過程,當所有 Bean 創建完成之後,調用 context#start 將會發送 ContextStartedEvent 。這裏源碼比較簡單,如下:

public void start() {
    getLifecycleProcessor().start();
    publishEvent(new ContextStartedEvent(this));
}

4.2、銷毀過程

調用 ClassPathXmlApplicationContext#close 方法將會關閉容器,具體邏輯將會在 doClose 方法執行。

doClose 這個方法首先發送 ContextClosedEvent,然再后開始銷毀 Bean

靈魂拷問:如果我們顛倒上面兩者順序,結果會一樣嗎?

doClose 源碼如下:

protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 省略無關代碼

        try {
            // Publish shutdown event.
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }


        // 銷毀 Bean
        destroyBeans();

        // 省略無關代碼
    }
}

destroyBeans 最終將會執行 DisposableBeanAdapter#destroy@PreDestroyDisposableBeandestroy-method 三者定義的方法都將會在內部被執行。

首先執行 DestructionAwareBeanPostProcessor#postProcessBeforeDestruction,這裏方法類似與上面 BeanPostProcessor

@PreDestroy 註解將會被 CommonAnnotationBeanPostProcessor 攔截,這裏類同時也繼承了 DestructionAwareBeanPostProcessor

最後如果 BeanDisposableBean 的子類,將會執行 destroy 方法,如果在 xml 定義了 destroy-method 方法,該方法也會被執行。

public void destroy() {
    if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
        for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
            processor.postProcessBeforeDestruction(this.bean, this.beanName);
        }
    }

    if (this.invokeDisposableBean) {
        // 省略無關代碼
        // 如果 Bean 繼承 DisposableBean,執行 destroy 方法
        ((DisposableBean) bean).destroy();
        
    }

    if (this.destroyMethod != null) {
        // 執行 xml 指定的  destroy-method 方法
        invokeCustomDestroyMethod(this.destroyMethod);
    }
    else if (this.destroyMethodName != null) {
        Method methodToCall = determineDestroyMethod();
        if (methodToCall != null) {
            invokeCustomDestroyMethod(methodToCall);
        }
    }
}

五、總結

init-method/destroy-method 這種方式需要使用 XML 配置文件或單獨註解配置類,相對來說比較繁瑣。而InitializingBean/DisposableBean 這種方式需要單獨繼承 Spring 的接口實現相關方法。@PostConstruct/@PreDestroy 這種註解方式使用方式簡單,代碼清晰,比較推薦使用這種方式。

另外 ContextStartedEvent/ContextClosedEvent 這種方式比較適合在一些集成框架使用,比如 Dubbo 2.6.X 優雅停機就是用改機制。

六、Spring 歷史文章推薦

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

設計模式之代理模式

什麼是代理模式

代理模式就是為一個對象提供一個代理對象,由這個代理對象控制對該對象的訪問。

理解代理模式,可以對照生活中的一些具體例子,比如房產中介、二手車交易市場、經紀人等。

為什麼要用代理模式

通過使用代理模式,我們避免了直接訪問目標對象時可能帶來的一些問題,比如:遠程調用,需要使用遠程代理來幫我們處理一些網絡傳輸相關的細節邏輯;可能需要基於某種權限控制對目標資源的訪問,可以使用保護代理等。

總的來說,通過是用代理模式,我們可以控制對目標對象的訪問,可以在真實方法被調用前或調用后,通過代理對象加入額外的處理邏輯。

代理模式分類

代理模式分為靜態代理和動態代理。動態代理根據實現不同又可細分為JDK動態代理和cglib動態代理。

靜態代理是由程序員創建或工具生成代理類的源碼,再編譯代理類。所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委託類的關係在運行前就確定了。

動態代理是在實現階段不用關心代理類,而在運行時動態生成代理類的。

靜態代理

以房哥買房子為例,用代碼實現靜態代理。

1、首先建立一個Seller接口

public interface Seller {
    void sell();
}

2、創建實現類,房哥,有一個方法,就是買房子

public class FangGe implements Seller{
    @Override
    public void sell() {
        System.out.println("房哥要出手一套四合院");
    }
}

3、買房子需要找到買家,達成交易后還要辦理過戶等其他手續,房哥只想賣房收錢就完了。因此,需要找一個代理來幫房哥處理這些雜事。

我們創建一個代理類FangGeProxy,代理類也需要實現Seller接口,行為上要保持和FangGe一樣,都是要賣房子。同時該代理類還需要持有房哥的引用。

public class FangGeProxy implements Seller{
    private FangGe fangGe;

    public FangGeProxy(FangGe fangGe){
        this.fangGe = fangGe;
    }
    @Override
    public void sell() {
        findBuyer();
        fangGe.sell();
        afterSell();
    }
    
    public void findBuyer(){
        System.out.println("代理幫助尋找買主");
    }
    
    public void afterSell(){
        System.out.println("達成交易后,辦理相關手續");
    }
}

可以看到,房哥的代理類通過findBuyer()和afterSell()兩個方法幫助房哥完成了其他一些雜事。

4、測試類

public class StaticProxyTest {
    public static void main(String[] args) {
        Seller seller = new FangGeProxy(new FangGe());
        seller.sell();
    }
}

輸出:

代理幫助尋找買主
房哥要出手一套四合院
達成交易后,辦理相關手續

最後,看下類圖

靜態代理的問題:

1、由於靜態代理類在編譯前已經確定了代理的對象,因此靜態代理只能代理一種類型的類,如果要給大量的類做代理,就需要編寫大量的代理類;

2、如果我們要給Seller,也就是目標對象要增加一些方法,則需要同步修改代理類,不符合開閉原則。

JDK動態代理

JDK的動態代理依賴於jdk給我們提供的類庫實現,是一種基於接口實現的動態代理,在編譯時並不知道要代理哪個類,而是在運行時動態生成代理類。同時也解決了靜態代理中存在的問題。

我們接上上面靜態代理的例子,繼續實現JDK的動態代理。

1、我們建一個方法轉發的處理器類,該類需要實現InvocationHandler接口。

public class SellerInvocationHandler implements InvocationHandler {

    // 要代理的真實對象
    private Object target;

    /**
     * 使用Proxy類靜態方法獲取代理類實例
     */
    public Object getProxyInstance(Object target){
        this.target = target;
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(this.target, args);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法前");
    }
    
    private void after() {
        System.out.println("執行方法后");
    }
}

2、新建JDK動態代理測試類,首先代理房哥賣房子

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        
        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();

    }
}

輸出:

執行方法前
房哥要出手一套四合院
執行方法后

可以看到,完成了代理。

3、接下來我們新建另外一個類,User類,並使用JDK動態代理完成代理User類

public interface IUser {
    void sayHello();

    void work();
}

public class UserImpl implements IUser{
    @Override
    public void sayHello() {
        System.out.println("hello,我是小明");
    }

    @Override
    public void work() {
        System.out.println("我正在寫代碼");
    }
}

修改測試類,

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

/*        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();

        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();*/

        // 代理user類
        IUser user = new UserImpl();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        IUser userProxy = (IUser) sellerInvocationHandler.getProxyInstance(user);
        userProxy.sayHello();
        userProxy.work();

    }
}

輸出:

執行方法前
hello,我是小明
執行方法后
執行方法前
我正在寫代碼
執行方法后

可以看到,我們SellerInvocationHandler 並未做任何改動,它便能為UserImpl類生成代理,並在執行方法的前後增加額外的執行邏輯。

cglib動態代理

JDK動態代理有一個局限就是,被代理的類必須要實現接口。如果被代理的類沒有實現接口,則JDK動態代理就無能為力了。這個時候該cglib動態代理上場了。

CGLIB是一個功能強大,高性能的代碼生成包。它為沒有實現接口的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理創建代理,但當要代理的類沒有實現接口或者為了更好的性能,CGLIB是一個好的選擇。

1、新建一個MyCglibInterceptor,實現MethodInterceptor接口。該類類似於JDK動態代理中的InvocationHandler實例,是實現cglib動態代理的主要類。

public class MyCglibInterceptor implements MethodInterceptor {

    public Object getCglibProxyInstance(Object object){
        // 相當於Proxy,創建代理的工具類
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(object.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o, objects);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法之前");
    }

    private void after() {
        System.out.println("執行方法之後");
    }
}

2、新建cglib動態代理的測試類,先代理上面例子中的User類。

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();
    }
}

輸出:

執行方法之前
hello,我是小明
執行方法之後
執行方法之前
我正在寫代碼
執行方法之後

3、新建一個類HelloWorld,不實現任何接口,為該類實現動態代理。

public class HelloWorld {
    public void hello(){
        System.out.println("世界這麼大,我想去看看");
    }
}

測試代理類

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
/*        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();*/

        // 代理未實現任何接口的類
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        HelloWorld helloWorldProxy = (HelloWorld) myCglibInterceptor.getCglibProxyInstance(new HelloWorld());
        helloWorldProxy.hello();
    }
}

輸出:

執行方法之前
世界這麼大,我想去看看
執行方法之後

使用cglib動態代理,我們實現了對普通類的代理。

(完)

設計模式系列文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

Java併發之volatile關鍵字

引言

說到多線程,我覺得我們最重要的是要理解一個臨界區概念。

舉個例子,一個班上1個女孩子(臨界區),49個男孩子(線程),男孩子的目標就是這一個女孩子,就是會有競爭關係(線程安全問題)。推廣到實際場景,例如對一個數相加或者相減等等情形,因為操作對象就只有一個,在多線程環境下,就會產生線程安全問題。理解臨界區概念,我們對多線程問題可以有一個好意識。

Jav內存模型(JMM)

談到多線程就應該了解一下Java內存模型(JMM)的抽象示意圖.下圖:

線程A和線程B執行的是時候,會去讀取共享變量(臨界區),然後各自拷貝一份回到自己的本地內存,執行後續操作。
JMM模型是一種規範,就像Java的接口一樣。JMM會涉及到三個問題:原子性,可見性,有序性。
所謂原子性。就是說一個線程的執行會不會被其他線程影響的。他是不可中斷的。舉個例子:

int i=1

這個語句在Jmm中就是原子性的。無論是一個線程執行還是多個線程執行這個語句,讀出來的i就是等於1。那什麼是非原子性呢,按道理如果Java的代碼都是原子性,應該就不會有線程問題了啊。其實JMM這是規定某些語句是原子性罷了。舉個非原子性例子:

i ++;

這個操作就不是原子性的了。因為他就是包含了三個操作:第一讀取i的值,第二將i加上1,第三將結果賦值回來給i,更新i的值。
所謂可見性。可見性表示如果一個值在線程A修改了,線程B就會馬上知道這個結果。
所謂有序性。所謂有序性值的是語意的有序性。就是說代碼順序可能會發生變化。因為有一個指令重排機制。所謂指令重排,他會改變代碼執行順序,為了讓cpu執行效率更高。為了防止重排序出錯,JMM有個happen-before規則,這個規則限制了那些語句執行在前,那些語句執行在後。
Happen-before:
程序順序原則:一個線程內保證語義的串行性
volatile原則:volatile變量的寫發生在讀之前
鎖規則:先加鎖再解鎖
傳遞性:a先於b,b先於c,則a必定先於c
線程的start方法先於他的每一個操作
線程所有的操作先於線程的終結
對象的構造函數執行、結束先於finalize()方法。

volatile

進入正題,volatile可以保證變量(臨界區)的可見性以及有序性,但是不能保證原子性。舉個例子:

public class VolatileTest implements Runnable{
    private static VolatileTest volatileTest = new VolatileTest();
    private  static volatile int i= 0;
    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < 20; j++) {
            Thread a = new Thread(new VolatileTest());
            Thread b = new Thread(new VolatileTest());
            a.start();b.start();
            a.join();b.join();
            System.out.print(i+"&&");
        }

    }
    
    @Override
    public void run() {
        for (int j = 0; j < 1000; j++) {
            i++;
        }
    }

}

// 輸出結果
// 2000&&4000&&5852&&7852&&9852&&11852&&13655&&15655&&17655&&19655&&21306     
//&&22566&&24566&&26189&&28189&&30189&&32189&&34189&&36189&&38089&&

有結果看到有問題,雖然i已經添加了volatile關鍵字,說明volatile關鍵字不能保證i++的原子性。

那什麼場景適合使用volatile關鍵字

  1. 輕量級的“讀-寫鎖”策略
private volatile int value;
public int getValue(){ return value;}
public synchronized void doubleValue(){ value = value*value; }

2.單例模式(雙檢查鎖機制

private volatile static Singleton instace;   
public static Singleton getInstance(){  // 沒有使用同步方法,而是同步方法塊
    //第一次null檢查 ,利用volatile的線程間可見性,不需要加鎖,性能提高    
    if(instance == null){            
        synchronized(Singleton.class) {    //鎖住類對象,阻塞其他線程
            //第二次null檢查,以保證不會創建重複的實例       
            if(instance == null){       
                instance = new Singleton(); // 禁止重排序
            }  
        }           
    }  
    return instance;

參考

《現代操作系統(第三版)中文版》
《實戰Java高併發程序設計》
《Java併發編程的藝術》

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

EF Core For MySql查詢中使用DateTime.Now作為查詢條件的一個小問題

背景

最近一直忙於手上澳洲線上項目的整體遷移和升級的準備工作,導致博客和公眾號停更。本周終於艱難的完成了任務,藉此機會,總結一下項目中遇到的一些問題。

EF Core一直是我們團隊中中小型項目常用的ORM框架,在使用SQL Server作為持久化倉儲的場景一下,一直表現還中規中矩。但是在本次項目中,項目使用了MySql作為持久化倉儲。為了與EF Core集成,團隊使用了Pomelo.EntityFrameworkCore.MySql作為EF Core For MySql的擴展。在開發過程中,團隊遇到了各種各樣在SQL Server場景下沒有遇到過的問題,其中最奇怪的,也是隱藏最深的問題,就是將DateTime.Now作為查詢條件,產生了非預期的結果。

問題場景

本周在項目升級的過程中,客戶反饋了一個問題。

在當前系統的Dashboard頁面,有一個消息提醒功能,客戶可以自定義一些消息,並且指定提醒的日期。客戶遇到的問題是通常添加的消息提醒,在指定日期的上午時間段是不會显示,只有在下午時間段才能看到,比如說客戶指定2019年10月26號看到一個的消息提醒,但是在10月26日這天早上8:00-12:00這個時間段,系統總是看不到提醒,只有到了下午的時間段才能看到提醒。

PS:這裏客戶表達的只是個籠統的問題,但問題確實是上午的大部分時間是看不到消息提醒的,但並不是精確到中午12:00點這個時間, 所以此處不必過於糾結於具體的時間。

查看問題代碼

看到這個問題的時候,我自己也很奇怪,難道代碼或者數據庫使用了時區,導致查詢出現了偏差?

於是我就Review了一下此處的查詢, 代碼如下。

var query = DbContext.CRM_Note_Reminders
    .Include(x => x.CRM_Note)
    .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= DateTime.Now.Date)
     .ToList();

PS: 這裏可能有同學會有疑問,為啥不用DbFunctions.DiffDays? 原因是DbFunctions.DiffDays是 EF Core for SQLServer的擴展方法,針對MySql還沒有官方的實現方案。

從這個查詢中,我沒有看出任何問題,於是我直接藉助一些日誌工具,將EF Core生成的查詢語句的輸出了出來。

其中WHERE條件部分如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE))
AND (CONVERT(`x`.`Reminder_Date`, date) 
  <= CONVERT(CURRENT_TIMESTAMP(), date)))

這裏CURRENT_TIMESTAMP()是MySql的內置函數,與SQLServer的內置函數GETDATE()不同,CURRENT_TIMESTAMP()默認返回的是UTC時間。因此我們大概能知道,為什麼澳洲客戶會遇到上面的場景了。

PS: 根據7樓兄弟的反饋,我試了一下,改動Mysql的時區配置之後,果然CURRENT_TIMESTAMP()就改為了對應時區的時間。這裏使用UTC時間的原因應該是我在AWS RDS上創建Mysql實例的時候,忽略了時區配置。

由於澳洲處於東10區,與UTC時間有+10個小時的時差,所以當澳洲上午的10點之前,UTC時間都是在當前澳洲日期的前一天,所以系統中出現了當天的消息提醒在上午時間段不能正常显示的問題。

PS: 由於澳洲是分冬令時和夏令時的,夏令時時間要加一個小時,所以實際上客戶在每天的11點之前都無法看到正確的消息提醒。

深入思考

你這可能會非常奇怪,為什麼DateTime.Now會被轉化成內置函數CURRENT_TIMESTAMP(),而沒有使用我們傳入的值DateTime.Now.Date呢?

其實EF/EF Core在查詢是時候是分2個階段的,一個是組合查詢表達式樹的階段,一個是真正的查詢階段。

在組合查詢表達式樹的階段,EF/EF Core只會去組合表達式,而不會去嘗試計算表達式的值,所以這個階段DateTime.Now.Date的值並沒有被計算出來, 在進入正常查詢階段的時候, EF/EF Core會嘗試將查詢表達式樹翻譯成SQL腳本,這時候由於我們的EF ProviderMySql Provider, 恰巧DateTime.Now可以翻譯成Mysql的內置函數CURRENT_TIMESTAMP(), 所以這裏EF/EF Core就跳過了表達式值的計算,直接將其翻譯成了對應的內置函數,所以導致生成的SQL查詢和我們的預期有偏差。

那麼我們該如何解決這個問題呢?

解決方案

經過了以上的思考,其實解決這個問題也就很簡單了,我們可以將DateTime.Now.Date先計算出來,保存在一個變量中,然後將這個變量傳入查詢中。

var today = DateTime.Now.Date;

var query = DbContext.CRM_Note_Reminders
     .Include(x => x.CRM_Note)
     .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= today)
     .ToList();

由此生成的MySQL腳本如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE)) 
AND (CONVERT(`x`.`Reminder_Date`, date) <= @__date_0)) 

這樣我們就得到了一個正確的結果,澳洲客戶也就收到了正確的消息。

是不是有種差之毫厘,謬以千里的感覺呢?

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

Java描述設計模式(23):訪問者模式

本文源碼: ||

一、生活場景

1、場景描述

電競是遊戲比賽達到“競技”層面的體育項目。利用电子設備作為運動器械進行的、人與人之間的智力對抗運動。通過電競,可以提高人的反應能力、協調能力、團隊精神等。但是不同人群的對電競的持有的觀念不一樣,有的人認為電競就是沉迷網絡,持反對態度,而有的人就比較贊同。下面基於訪問者模式來描述該場景。

2、場景圖解

3、代碼實現

public class C01_InScene {
    public static void main(String[] args) {
        DataSet dataSet = new DataSet() ;
        dataSet.addCrowd(new Youth());
        dataSet.addCrowd(new MiddleAge());
        CrowdView crowdView = new Against() ;
        dataSet.display(crowdView);
        crowdView = new Approve() ;
        dataSet.display(crowdView);
    }
}
/**
 * 雙分派,不同人群管理
 */
abstract class Crowd {
    abstract void accept(CrowdView action);
}
class Youth extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getYouthView(this);
    }
}
class MiddleAge extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getMiddleAgeView (this);
    }
}
/**
 * 不同人群觀念的管理
 */
abstract class CrowdView {
    // 青年人觀念
    abstract void getYouthView (Youth youth);
    // 中年人觀念
    abstract void getMiddleAgeView (MiddleAge middleAge);
}
class Approve extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人贊同電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人贊同電競");
    }
}
class Against extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人反對電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人反對電競");
    }
}
/**
 * 提供一個數據集合
 */
class DataSet {
    private List<Crowd> crowdList = new ArrayList<>();
    public void addCrowd (Crowd crowd) {
        crowdList.add(crowd);
    }
    public void display(CrowdView crowdView) {
        for(Crowd crowd : crowdList) {
            crowd.accept(crowdView);
        }
    }
}

二、訪問者模式

1、基礎概念

訪問者模式是對象的行為模式,把作用於數據結構的各元素的操作封裝,操作之間沒有關聯。可以在不改變數據結構的前提下定義作用於這些元素的不同的操作。主要將數據結構與數據操作分離,解決數據結構和操作耦合問題核心原理:被訪問的類裏面加對外提供接待訪問者的接口。

2、模式圖解

3、核心角色

  • 抽象訪問者角色

聲明多個方法操作,具體訪問者角色需要實現的接口。

  • 具體訪問者角色

實現抽象訪問者所聲明的接口,就是各個訪問操作。

  • 抽象節點角色

聲明接受操作,接受訪問者對象作為參數。

  • 具體節點角色

實現抽象節點所規定的具體操作。

  • 結構對象角色

能枚舉結構中的所有元素,可以提供一個高層的接口,用來允許訪問者對象訪問每一個元素。

4、源碼實現

public class C02_Visitor {
    public static void main(String[] args) {
        ObjectStructure obs = new ObjectStructure();
        obs.add(new NodeA());
        obs.add(new NodeB());
        Visitor visitor = new VisitorA();
        obs.doAccept(visitor);
    }
}
/**
 * 抽象訪問者角色
 */
interface Visitor {
    /**
     * NodeA的訪問操作
     */
    void visit(NodeA node);
    /**
     * NodeB的訪問操作
     */
    void visit(NodeB node);
}
/**
 * 具體訪問者角色
 */
class VisitorA implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
class VisitorB implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
/**
 * 抽象節點角色
 */
abstract class Node {
    /**
     * 接收訪問者
     */
    abstract void accept(Visitor visitor);
}
/**
 * 具體節點角色
 */
class NodeA extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationA(){
        System.out.println("NodeA.operationA");
    }
}
class NodeB extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationB(){
        System.out.println("NodeB.operationB");
    }
}
/**
 * 結構對象角色類
 */
class ObjectStructure {
    private List<Node> nodes = new ArrayList<>();
    public void detach(Node node) {
        nodes.remove(node);
    }
    public void add(Node node){
        nodes.add(node);
    }
    public void doAccept(Visitor visitor){
        for(Node node : nodes) {
            node.accept(visitor);
        }
    }
}

三、Spring框架應用

1、Bean結構的訪問

BeanDefinitionVisitor類,遍歷bean的各個屬性;接口 BeanDefinition,定義Bean的各樣信息,比如屬性值、構造方法、參數等等。這裏封裝操作bean結構的相關方法,但卻沒有改變bean的結構。

2、核心代碼塊

public class BeanDefinitionVisitor {
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        this.visitParentName(beanDefinition);
        this.visitBeanClassName(beanDefinition);
        this.visitFactoryBeanName(beanDefinition);
        this.visitFactoryMethodName(beanDefinition);
        this.visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            this.visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            this.visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }
}

四、模式總結

1、優點描述

(1) 訪問者模式符合單一職責原則、使程序具有良好的擴展性、靈活性;

(2) 訪問者模式適用與攔截器與過濾器等常見功能,數據結構相對穩定的場景;

2、缺點描述

(1) 訪問者關注其他類的內部細節,依賴性強,違反迪米特法則,這樣導致具體元素更新麻煩;

(2) 訪問者依賴具體元素,不是抽象元素,面向細節編程,違背依賴倒轉原則;

五、源代碼地址

GitHub·地址
https://github.com/cicadasmile/model-arithmetic-parent
GitEE·地址
https://gitee.com/cicadasmile/model-arithmetic-parent

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

eMOVING 極速 100 公里電動機車 iE125 上市,快充 10 分鐘可騎 78 公里

中華汽車在 7 月 19 日發表新款電動機車 eMOVING iE125,極速可達到時速 100 公里,成為市場關注的焦點。

iE 125 的命名除了代表 intelligent electricity,也有 industrial engineering 的含義,125 則象徵 125cc 等級燃油機車的性能與操控。iE125 安全極速達到時速 100 公里,靜止加速到時速 50 公里僅需 3.9 秒,30% 坡度爬坡最高速為時速 32 公里。iE125 在充滿電之後,時速 30 公里之下續航里程為 155 公里,TES 變速續航里程為 82 公里。支援超級快充功能,充電 10 分鐘就能充滿 50% 的電力,可以行駛 78 公里。

iE125 含電池車重為 124 公斤,配備 CBS 連動煞車和 IP67 防水等級。提供 ECO、SPEED 和 BOOST 三種行車模式,讓消費者在不同情境下使用。特別的電動駐車功能只要按下按鈕就能直接立起中柱,車主不再需要為立中柱而困擾。配置 QC 3.0 USB 充電座,方便騎乘時進行充電。

eMOVING iE125 藍色版。

eMOVING iE125 白色版。

eMOVING iE125 橘色版。

eMOVING iE125 灰色版。

iE125 精緻型儀錶板螢幕為彩色液晶螢幕,豪華型和旗艦型則為汽車級 TFT。豪華型和旗艦型搭載車輛診斷系統,能在儀表板上顯示車身、動力、電池和胎壓等資訊,並提供異常提示與保養提醒。儀表板還可以進行個人化設定,自由更換儀表板主題與桌布。

iE125 豪華型和旗艦型具有遙控防盜中控鎖,能夠連結手機進行上鎖、解鎖和座墊開啟。iOS 版本的 App 支援即時來電提醒,來電與訊息通知會即時顯示。旗艦型還特別內建前方行車記錄器和胎壓偵測器,進一步確保行車安全。

儀錶板能顯示各種車身資訊。

eMOVING 將充電分為 3 種類型,分別在不同需求時使用。家用滿足平時充電需求,約 160 分鐘可以充滿 50% 的電力。快速充電站則供在外逛街或用餐時補充電力,約 30 分鐘可以充滿 50% 的電力。超級充電站供臨時路途中繼充電,約 10 分鐘可以充滿 50% 的電力。

消費者可以自行選擇電池租賃方案,eMOVING 提供電池永久保固。基礎型在家充電每月 399 元,輕量型每月 599 元額外提供 100 分鐘的超級充電分鐘數,進階型每月 799 元可以不限時數進行超級充電。為了推廣超級充電,12 月以前輕量型和進階型方案皆以每月 499 元計價,而且享受不限時數的超級充電。

中華汽車預計在 12 月佈建 70 座以上的快速充電站,2020 年 6 月更要佈建超過 150 座快速充電站,早期合作夥伴包括肯德基、家樂福、順益汽車和滙豐汽車。中華汽車也宣布捐贈 5 座超級充電站給桃園市政府,未來讓符合快充共通規格電動機車車主都能免費充電。

中華汽車捐贈 5 座超級充電站給桃園市政府。

eMOVING 推出了 10 月底前購車,就贈送 5,000 元購車金的優惠,可以全額折抵車價或購買配件。iE125 提供藍色、白色、灰色和橘色 4 種顏色讓消費者選擇,精緻型定價為台幣 73,800 元,豪華型定價為台幣 79,800 元,旗艦型定價為台幣 85,800 元。補助最高的桃園市汰換二行程機車換購電動機車補助最高 29,000 元,再加上 10 月底前購車贈送的購車金 5,000 元,精緻型最低台幣 39,800 元起,豪華型最低台幣 45,800 元起,旗艦型最低台幣 51,800 元起。

(合作媒體:。圖片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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