月份: 2020 年 3 月

  • [ASP.NET Core 3框架揭秘] 文件系統[3]:物理文件系統

    [ASP.NET Core 3框架揭秘] 文件系統[3]:物理文件系統

    ASP.NET Core應用中使用得最多的還是具體的物理文件,比如配置文件、View文件以及作為Web資源的靜態文件。物理文件系統由定義在NuGet包“Microsoft.Extensions.FileProviders.Physical”中的PhysicalFileProvider來構建。我們知道System.IO命名空間下定義了一整套針操作物理目錄和文件的API,實際上PhysicalFileProvider最終也是通過調用這些API來完成相關的IO操作。

    public class PhysicalFileProvider : IFileProvider, IDisposable
    {   
        public PhysicalFileProvider(string root);   
         
        public IFileInfo GetFileInfo(string subpath);  
        public IDirectoryContents GetDirectoryContents(string subpath); 
        public IChangeToken Watch(string filter);
    
        public void Dispose();   
    }

    一、PhysicalFileInfo

    一個PhysicalFileProvider對象總是映射到某個具體的物理目錄上,被映射的目錄所在的路徑通過構造函數的參數root來提供,該目錄將作為PhysicalFileProvider的根目錄。GetFileInfo方法返回的IFileInfo對象代表指定路徑對應的文件,這是一個類型為PhysicalFileInfo的對象。一個物理文件可以通過一個System.IO.FileInfo對象來表示,一個PhysicalFileInfo對象實際上就是對該對象的封裝,定義在PhysicalFileInfo的所有屬性都來源於這個FileInfo對象。對於創建讀取文件輸出流的CreateReadStream方法來說,它返回的是一個根據物理文件絕對路徑創建的FileStream對象。

    public class PhysicalFileInfo : IFileInfo
    {
        ...
        public PhysicalFileInfo(FileInfo info);    
    }

    對於PhysicalFileProvider的GetFileInfo方法來說,即使我們指定的路徑指向一個具體的物理文件,它並不總是會返回一個PhysicalFileInfo對象。PhysicalFileProvider會將一些場景視為“目標文件不存在”,並讓GetFileInfo方法返回一個NotFoundFileInfo對象。具體來說,PhysicalFileProvider的GetFileInfo方法在如下的場景中會返回一個NotFoundFileInfo對象:

    • 確實沒有一個物理文件與指定的路徑相匹配。
    • 如果指定的是一個絕對路徑(比如“c:\foobar”),即Path.IsPathRooted方法返回True。
    • 如果指定的路徑指向一個隱藏文件。

    顧名思義,具有如下定義的NotFoundFileInfo類型表示一個“不存在”的文件。NotFoundFileInfo對象的Exists屬性總是返回False,而其他的屬性則變得沒有任何意義。當我們調用它的CreateReadStream試圖讀取一個根本不存在的文件內容時,會拋出一個FileNotFoundException類型的異常。

    public class NotFoundFileInfo : IFileInfo
    {
        public bool Exists => false;   
        public long Length => throw new NotImplementedException();   
        public string PhysicalPath => null;  
        public string Name { get; }   
        public DateTimeOffset LastModified => DateTimeOffset.MinValue;
        public bool IsDirectory => false; 
    
        public NotFoundFileInfo(string name) => this.Name = name;
        public Stream CreateReadStream() => throw new FileNotFoundException($"The file {Name} does not exist.");
    }

    二、PhysicalDirectoryInfo

    PhysicalFileProvider利用一個PhysicalFileInfo對象來描述某個具體的物理文件,而一個物理目錄則通過一個PhysicalDirectoryInfo的對象來描述。既然PhysicalFileInfo是對一個FileInfo對象的封裝,那麼我們應該想得到PhysicalDirectoryInfo對象封裝的就是表示目錄的DirectoryInfo對象。如下面的代碼片段所示,我們需要在創建一個PhysicalDirectoryInfo對象時提供這個DirectoryInfo對象,PhysicalDirectoryInfo實現的所有屬性的返回值都來源於這個DirectoryInfo對象。由於CreateReadStream方法的目的總是讀取文件的內容,所以PhysicalDirectoryInfo類型的這個方法會拋出一個InvalidOperationException類型的異常。

    public class PhysicalDirectoryInfo : IFileInfo
    {   
        ...
        public PhysicalDirectoryInfo(DirectoryInfo info);
    }

    三、PhysicalDirectoryContents

    當我們調用PhysicalFileProvider的GetDirectoryContents方法時,如果指定的路徑指向一個具體的目錄,那麼該方法會返回一個類型為PhysicalDirectoryContents的對象。PhysicalDirectoryContents是一個IFileInfo對象的集合,該集合中包括所有描述子目錄的PhysicalDirectoryInfo對象和描述文件的PhysicalFileInfo對象。PhysicalDirectoryContents的Exists屬性取決於指定的目錄是否存在。

    public class PhysicalDirectoryContents : IDirectoryContents
    {
        public bool Exists { get; }
        public PhysicalDirectoryContents(string directory);
        public IEnumerator<IFileInfo> GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator();
    }

    四、NotFoundDirectoryContents

    如果指定的路徑並不指向一個存在的目錄,或者指定的是一個絕對路徑,GetDirectoryContents方法都會返回一個Exsits為False的NotFoundDirectoryContents對象。如下所示的代碼片段展示了NotFoundDirectoryContents類型的定義,如果我們需要使用到這麼一個類型,可以直接利用靜態屬性Singleton得到對應的單例對象。

    public class NotFoundDirectoryContents : IDirectoryContents
    {    
        public static NotFoundDirectoryContents Singleton { get; }  = new NotFoundDirectoryContents();
        public bool Exists => false;
        public IEnumerator<IFileInfo> GetEnumerator()  => Enumerable.Empty<IFileInfo>().GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    五、PhysicalFilesWatcher

    我們接着來談談PhysicalFileProvider的Watch方法。當我們調用該方法的時候,PhysicalFileProvider會通過解析我們提供的Globbing Pattern表達式來確定我們期望監控的文件或者目錄,並最終利用FileSystemWatcher對象來對這些文件實施監控。這些文件或者目錄的變化(創建、修改、重命名和刪除等)都會實時地反映到Watch方法返回的IChangeToken上。

    PhysicalFileProvider的Watch方法中指定的Globbing Pattern表達式必須是針對當前根目錄的相對路徑,我們可以使用“/”或者“./”前綴,也可以不採用任何前綴。一旦我們使用了絕對路徑(比如“c:\test\*.txt”)或者“../”前綴(比如“../test/*.txt”),不論解析出來的文件是否存在於PhysicalFileProvider的根目錄下,這些文件都不會被監控。除此之外,如果我們沒有指定Globbing Pattern表達式,PhysicalFileProvider也不會有任何的文件會被監控。

    PhysicalFileProvider針對物理文件系統變化的監控是通過如下這個PhysicalFilesWatcher對象實現的,其Watch方法內部會直接調用PhysicalFileProvider的CreateFileChangeToken方法,並返回得到的IChangeToken對象。這是一個公共類型,如果我們具有監控物理文件系統變化的需要,可以直接使用這個類型。

    public class PhysicalFilesWatcher: IDisposable
    {
        public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher, bool pollForChanges);
        public IChangeToken CreateFileChangeToken(string filter);
        public void Dispose();
    }

    從PhysicalFilesWatcher構造函數的定義我們不難看出,它最終是利用一個FileSystemWatcher對象(對應於fileSystemWatcher參數)來完成針對指定根目錄下(對應於root參數)所有子目錄和文件的監控。FileSystemWatcher的CreateFileChangeToken方法返回的IChangeToken對象會幫助我們感知到子目錄或者文件的添加、刪除、修改和重命名,但是它會忽略隱藏的目錄和文件。最後需要提醒的是,當我們不再需要對指定目錄實施監控的時候,記得調用PhysicalFileProvider的Dispose方法,該方法會負責將FileSystemWatcher對象關閉。

    六、小結

    我們藉助下圖所示的UML來對由PhysicalFileProvider構建物理文件系統的整體設計做一個簡單的總結。首先,該文件系統使用PhysicalDirectoryInfo和PhysicalFileInfo對類型來描述目錄和文件,它們分別是對DirectoryInfo和FileInfo(System.IO.FileInfo)對象的封裝。

    PhysicalFileProvider的GetDirectoryContents方法返回一個PhysicalDirectoryContents 對象(如果指定的目錄存在),組成該對象的分別是根據其所有子目錄和文件創建的PhysicalDirectoryInfo和PhysicalFileInfo對象。當我們調用PhysicalFileProvider的GetFileInfo方法時,如果指定的文件存在,返回的是描述該文件的PhysicalFileInfo對象。至於PhysicalFileProvider的Watch方法,它最終利用了FileSystemWatcher來監控指定文件或者目錄的變化。

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

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

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

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

  • 面向對象和面向過程詳解

    1.前言

    其實一直對面向過程和面向對象的概念和區別沒有很深入的理解,在自己不斷想完善自己的知識體系中,今天借這個時間,寫一篇博客。來深入的了解面向過程與面向對象!好記性不如爛筆頭!!  

    2.面向對象與面向過程的區別

    面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就可以了;面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描敘某個事物在整個解決問題的步驟中的行為。

    舉一個下五子棋通俗例子吧 哈哈哈 我感覺這兒例子很容易讓人理解

    面向過程的設計思路就是首先分析問題的步驟:

    1、開始遊戲,

    2、黑子先走,

    3、繪製畫面,

    4、判斷輸贏,

    5、輪到白子,

    6、繪製畫面,

    7、判斷輸贏,

    8、返回步驟2,

    9、輸出最後結果。

    把上面每個步驟用分別的函數來實現,問題就解決了。

    面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為

    1、黑白雙方,這兩方的行為是一模一樣的,

    2、棋盤系統,負責繪製畫面,

    3、規則系統,負責判定諸如犯規、輸贏等。第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子布局的變化,

    棋盤對象接收到了棋子的i變化就要負責在屏幕上面显示出這種變化,同時利用第三類對象(規則系統)來對棋局進行判定。

    可以明顯地看出,面向對象是以功能來劃分問題,而不是步驟。同樣是繪製棋局,這樣的行為在面向過程的設計中分散在了總多步驟中,很可能出現不同的繪製版本,因為通常設計人員會考慮到實際情況進行各種各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。

    功能上的統一保證了面向對象設計的可擴展性。比如要加入悔棋的功能,如果要改動面向過程的設計,那麼從輸入到判斷到显示這一連串的步驟都要改動,甚至步驟之間的循序都要進行大規模調整。如果是面向對象的話,只用改動棋盤對象就行了,棋盤系統保存了黑白雙方的棋譜,簡單回溯就可以了,而显示和規則判斷則不用顧及,同時整個對對象功能的調用順序都沒有變化,改動只是局部的。

    再比如我要把這個五子棋遊戲改為圍棋遊戲,如果你是面向過程設計,那麼五子棋的規則就分佈在了你的程序的每一個角落,要改動還不如重寫。但是如果你當初就是面向對象的設計,那麼你只用改動規則對象就可以了,五子棋和圍棋的區別不就是規則嗎?(當然棋盤大小好像也不一樣,但是你會覺得這是一個難題嗎?直接在棋盤對象中進行一番小改動就可以了。)而下棋的大致步驟從面向對象的角度來看沒有任何變化。

    當然,要達到改動只是局部的需要設計的人有足夠的經驗,使用對象不能保證你的程序就是面向對象,初學者或者很蹩腳的程序員很可能以面向對象之虛而行面向過程之實,這樣設計出來的所謂面向對象的程序很難有良好的可移植性和可擴展性

    三、面向過程與面向對象的優缺點
    很多資料上全都是一群很難理解的理論知識,整的我頭都大了,後來發現了一個比較好的文章,寫的真是太棒了,通俗易懂,想要不明白都難!

    用面向過程的方法寫出來的程序是一份蛋炒飯,而用面向對象寫出來的程序是一份蓋澆飯。所謂蓋澆飯,北京叫蓋飯,東北叫燴飯,廣東叫碟頭飯,就是在一碗白米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。我覺得這個比喻還是比較貼切的。

    蛋炒飯製作是把米飯和雞蛋混在一起炒勻。蓋澆飯呢,則是把米飯和蓋菜分別做好,你如果要一份紅燒肉蓋飯呢,就給你澆一份紅燒肉;如果要一份青椒土豆蓋澆飯,就給澆一份青椒土豆絲。

    蛋炒飯的好處就是入味均勻,吃起來香。如果恰巧你不愛吃雞蛋,只愛吃青菜的話,那麼唯一的辦法就是全部倒掉,重新做一份青菜炒飯了。蓋澆飯就沒這麼多麻煩,你只需要把上面的蓋菜撥掉,更換一份蓋菜就可以了。蓋澆飯的缺點是入味不均,可能沒有蛋炒飯那麼香。

    到底是蛋炒飯好還是蓋澆飯好呢?其實這類問題都很難回答,非要比個上下高低的話,就必須設定一個場景,否則只能說是各有所長。如果大家都不是美食家,沒那麼多講究,那麼從飯館角度來講的話,做蓋澆飯顯然比蛋炒飯更有優勢,他可以組合出來任意多的組合,而且不會浪費。

    蓋澆飯的好處就是”菜”“飯”分離,從而提高了製作蓋澆飯的靈活性。飯不滿意就換飯,菜不滿意換菜。用軟件工程的專業術語就是”可維護性“比較好,”飯” 和”菜”的耦合度比較低。蛋炒飯將”蛋”“飯”攪和在一起,想換”蛋”“飯”中任何一種都很困難,耦合度很高,以至於”可維護性”比較差。軟件工程追求的目標之一就是可維護性,可維護性主要表現在3個方面:可理解性、可測試性和可修改性。面向對象的好處之一就是顯著的改善了軟件系統的可維護性。
    看了這篇文章,簡單的總結一下!

    面向過程

    優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如嵌入式開發、 Linux/Unix等一般採用面向過程開發,性能是最重要的因素。
    缺點:沒有面向對象易維護、易復用、易擴展
    面向對象

    優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易於維護

    缺點:性能比面向過程低

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

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

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

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

  • 堡壘機的核心武器:WebSSH錄像實現

    堡壘機的核心武器:WebSSH錄像實現

    WebSSH終端錄像的實現終於來了

    前邊寫了兩篇文章和深入介紹了終端錄製工具Asciinema,我們已經可以實現在終端下對操作過程的錄製,那麼在WebSSH中的操作該如何記錄並提供後續的回放審計呢?

    一種方式是文章最後介紹的自動錄製審計日誌的方法,在主機上添加個腳本,每次連接自動進行錄製,但這樣不僅要在每台遠程主機添加腳本,會很繁瑣,而且錄製的腳本文件都是放在遠程主機上的,後續播放也很麻煩

    那該如何更好處理呢?下文介紹一種優雅的方式來實現,核心思想是不通過錄製命令進行錄製,而在Webssh交互執行的過程中直接生成可播放的錄像文件

    設計思路

    通過上邊兩篇文章的閱讀,我們已經知道了Asciinema錄像文件主要由兩部分組成:header頭和IO流數據

    header頭位於文件的第一行,定義了這個錄像的版本、寬高、開始時間、環境變量等參數,我們可以在websocket連接創建時將這些參數按照需要的格式寫入到文件

    header頭數據如下,只有開頭一行,是一個字典形式

    {"version": 2, "width": 213, "height": 55, "timestamp": 1574155029.1815443, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}, "title": "ops-coffee"}

    整個錄像文件除了第一行的header頭部分,剩下的就都是輸入輸出的IO流數據,從websocket連接建立開始,隨着操作的進行,IO流數據是不斷增加的,直到整個websocket長連接的結束,那就需要在整個WebSSH交互的過程中不斷的往錄像文件追加輸入輸出的內容

    IO流數據如下,每一行一條,列表形式,分別表示操作時間,輸入或輸出(這裏我們為了方便就寫固定字符串輸出),IO數據

    [0.2341010570526123, "o", "Last login: Tue Nov 19 17:11:30 2019 from 192.168.105.91\r\r\n"]

    似乎很完美,按照上邊的思路錄像文件就應該沒有問題了,但還有一些細節需要處理

    首先是需要歷史連接列表,在這個列表裡可以看到什麼時間,哪個用戶連接了哪台主機,當然也需要提供回放功能,新建一張表來記錄這些信息

    class Record(models.Model):
        create_time = models.DateTimeField(auto_now_add=True, verbose_name='創建時間')
    
        host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name='主機')
        user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用戶')
    
        filename = models.CharField(max_length=128, verbose_name='錄像文件名稱')
    
        def __str__(self):
            return self.host

    其次還需要考慮的一個問題是header和後續IO數據流要寫入同一個文件,這就需要在整個websocket的連接過程中有一個固定的文件名可被讀取,這裏我使用了主機+用戶+當前時間作為文件名,同一用戶在同一時間不能多次連接同一主機,這樣可保證文件名不重複,同時避免操作寫入錯誤的錄像文件,文件名在websocket建立時初始化

    def __init__(self, host, user, websocket):
        self.host = host
        self.user = user
    
        self.time = time.time()
        self.filename = '%s.%s.%d.cast' % (host, user, self.time)

    IO流數據會持續不斷的寫入文件,這裏以一個獨立的方法來處理寫入

    def record(self, type, data):
        RECORD_DIR = settings.BASE_DIR + '/static/record/'
        if not os.path.isdir(RECORD_DIR):
            os.makedirs(RECORD_DIR)
    
        if type == 'header':
            Record.objects.create(
                host=Host.objects.get(id=self.host),
                user=self.user,
                filename=self.filename
            )
    
            with open(RECORD_DIR + self.filename, 'w') as f:
                f.write(json.dumps(data) + '\n')
        else:
            iodata = [time.time() - self.time, 'o', data]
            with open(RECORD_DIR + self.filename, 'a', buffering=1) as f:
                f.write((json.dumps(iodata) + '\n'))

    record接收兩個參數type和data,type標識本次寫入的是header頭還是IO流,data則是具體的數據

    header只需要執行一次寫入,所以將其放在ssh的connect方法中,只在ssh連接建立時執行一次,在執行header寫入時同時往數據庫插入新的歷史記錄數據

    調用record方法寫入header

    def connect(self, host, port, username, authtype, password=None, pkey=None,
                term='xterm-256color', cols=80, rows=24):
        ...
    
        # 構建錄像文件header
        self.record('header', {
            "version": 2,
            "width": cols,
            "height": rows,
            "timestamp": self.time,
            "env": {
                "SHELL": "/bin/bash",
                "TERM": term
            },
            "title": "ops-coffee"
        })

    IO流數據則需要與返回給前端的數據保持一致,這樣就能保證前端显示什麼錄像就播放什麼了,所以所有需要返回前端數據的地方都同時寫入錄像文件即可

    調用record方法寫入io流數據

    def connect(self, host, port, username, authtype, password=None, pkey=None,
                term='xterm-256color', cols=80, rows=24):
        ...
    
        # 連接建立一次,之後交互數據不會再進入該方法
        for i in range(2):
            recv = self.ssh_channel.recv(65535).decode('utf-8', 'ignore')
            message = json.dumps({'flag': 'success', 'message': recv})
            self.websocket.send(message)
    
            self.record('iodata', recv)
    
    ...
    
    def _ssh_to_ws(self):
        try:
            with self.lock:
                while not self.ssh_channel.exit_status_ready():
                    data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
                    if len(data) != 0:
                        message = {'flag': 'success', 'message': data}
                        self.websocket.send(json.dumps(message))
    
                        self.record('iodata', data)
                    else:
                        break
        except Exception as e:
            message = {'flag': 'error', 'message': str(e)}
            self.websocket.send(json.dumps(message))
            self.record('iodata', str(e))
            
            self.close()

    由於命令執行與返回都是多線程的操作,這就會導致在寫入文件時出現文件亂序影響播放的問題,典型的操作有vim、top等,通過加鎖self.lock可以順利解決

    最後歷史記錄頁面,當用戶點擊播放按鈕時,調用js彈出播放窗口

    <div class="modal fade" id="modalForm">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-body" id="play">
          </div>
        </div>
      </div>
    </div>
    
    // 播放錄像
    function play(host,user,time,file) {
      $('#play').html(
        '<asciinema-player id="play" title="WebSSH Record" author="ops-coffee.cn" author-url="https://ops-coffee.cn" author-img-url="/static/img/logo.png" src="/static/record/'+file+'" speed="3" '+
        'idle-time-limit="2" poster="data:text/plain,\x1b[1;32m'+time+
        '\x1b[1;0m用戶\x1b[1;32m'+user+
        '\x1b[1;0m連接主機\x1b[1;32m'+host+
        '\x1b[1;0m的錄像記錄"></asciinema-player>'
      )
    
      $('#modalForm').modal('show');
    }

    asciinema-player標籤的詳細參數介紹可以看這篇文章

    演示與總結

    在寫入文件的方案中,考慮了實時寫入和一次性寫入,實時寫入就像上邊這樣,所有的操作都會實時寫入錄像文件,好處是錄像不丟失,且能在操作的過程中進行實時的播放,缺點也很明顯,就是會頻繁的寫文件,造成IO開銷

    一次性寫入可以在用戶操作的過程中將錄像數據寫入內存,在websocket關閉時一次性異步寫入到文件中,這種方案在最終寫入文件時可能因為種種原因而失敗,從而導致錄像丟失,還有個缺點是當你WebSSH操作時間過長時,會導致內存的持續增加

    兩種方案一種是對磁盤的消耗另一種是對內存的消耗,各有利弊,當然你也可以考慮批量寫入,例如每分鐘寫一次文件,一分鐘之內的保存在內存中,平衡內存和磁盤的消耗,期待你的實現

    相關文章推薦閱讀:

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

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

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

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

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

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

  • NetCore3.0 文件上傳與大文件上傳的限制

    NetCore文件上傳兩種方式

      NetCore官方給出的兩種文件上傳方式分別為“緩衝”、“流式”。我簡單的說說兩種的區別,

      1.緩衝:通過模型綁定先把整個文件保存到內存,然後我們通過IFormFile得到stream,優點是效率高,缺點對內存要求大。文件不宜過大。

      2.流式處理:直接讀取請求體裝載后的Section 對應的stream 直接操作strem即可。無需把整個請求體讀入內存,

    以下為官方微軟說法

    緩衝

      整個文件讀入 IFormFile,它是文件的 C# 表示形式,用於處理或保存文件。 文件上傳所用的資源(磁盤、內存)取決於併發文件上傳的數量和大小。 如果應用嘗試緩衝過多上傳,站點就會在內存或磁盤空間不足時崩潰。 如果文件上傳的大小或頻率會消耗應用資源,請使用流式傳輸。

    流式處理   

      從多部分請求收到文件,然後應用直接處理或保存它。 流式傳輸無法顯著提高性能。 流式傳輸可降低上傳文件時對內存或磁盤空間的需求。

    文件大小限制

      說起大小限制,我們得從兩方面入手,1應用服務器Kestrel 2.應用程序(我們的netcore程序),

    1.應用服務器Kestre設置

      應用服務器Kestrel對我們的限制主要是對整個請求體大小的限制通過如下配置可以進行設置(Program -> CreateHostBuilder),超出設置範圍會報 BadHttpRequestException: Request body too large 異常信息

    public static IHostBuilder CreateHostBuilder(string[] args) =>
               Host.CreateDefaultBuilder(args)
                   .ConfigureWebHostDefaults(webBuilder =>
                   {
                       webBuilder.ConfigureKestrel((context, options) =>
                       {
                           //設置應用服務器Kestrel請求體最大為50MB
                           options.Limits.MaxRequestBodySize = 52428800;
                       });
                       webBuilder.UseStartup<Startup>();
    });

    2.應用程序設置

      應用程序設置 (Startup->  ConfigureServices) 超出設置範圍會報InvalidDataException 異常信息

    services.Configure<FormOptions>(options =>
     {
                 options.MultipartBodyLengthLimit = long.MaxValue;
     });

    通過設置即重置文件上傳的大小限制。

    源碼分析

      這裏我主要說一下 MultipartBodyLengthLimit  這個參數他主要限制我們使用“緩衝”形式上傳文件時每個的長度。為什麼說是緩衝形式中,是因為我們緩衝形式在讀取上傳文件用的幫助類為 MultipartReaderStream 類下的 Read 方法,此方法在每讀取一次後會更新下讀入的總byte數量,當超過此數量時會拋出  throw new InvalidDataException($Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.);  主要體現在 UpdatePosition 方法對 _observedLength  的判斷

    以下為 MultipartReaderStream 類兩個方法的源代碼,為方便閱讀,我已精簡掉部分代碼

    Read

    public override int Read(byte[] buffer, int offset, int count)
     {
              
              var bufferedData = _innerStream.BufferedData;
          int read;
          read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));
              return UpdatePosition(read);
    }

    UpdatePosition

    private int UpdatePosition(int read)
            {
                _position += read;
                if (_observedLength < _position)
                {
                    _observedLength = _position;
                    if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault())
                    {
                        throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");
                    }
                }
                return read;
    }

    通過代碼我們可以看到 當你做了 MultipartBodyLengthLimit 的限制后,在每次讀取後會累計讀取的總量,當讀取總量超出

     MultipartBodyLengthLimit  設定值會拋出 InvalidDataException 異常,

    最終我的文件上傳Controller如下

      需要注意的是我們創建 MultipartReader 時並未設置 BodyLengthLimit  (這參數會傳給 MultipartReaderStream.LengthLimit )也就是我們最終的限制,這裏我未設置值也就無限制,可以通過 UpdatePosition 方法體現出來

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.WebUtilities;
    using Microsoft.Net.Http.Headers;
    using System.IO;
    using System.Threading.Tasks;
     
    namespace BigFilesUpload.Controllers
    {
        [Route("api/[controller]")]
        public class FileController : Controller
        {
            private readonly string _targetFilePath = "C:\\files\\TempDir";
     
            /// <summary>
            /// 流式文件上傳
            /// </summary>
            /// <returns></returns>
            [HttpPost("UploadingStream")]
            public async Task<IActionResult> UploadingStream()
            {
     
                //獲取boundary
                var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value;
                //得到reader
                var reader = new MultipartReader(boundary, HttpContext.Request.Body);
                //{ BodyLengthLimit = 2000 };//
                var section = await reader.ReadNextSectionAsync();
     
                //讀取section
                while (section != null)
                {
                    var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
                    if (hasContentDispositionHeader)
                    {
                        var trustedFileNameForFileStorage = Path.GetRandomFileName();
                        await WriteFileAsync(section.Body, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                    }
                    section = await reader.ReadNextSectionAsync();
                }
                return Created(nameof(FileController), null);
            }
     
            /// <summary>
            /// 緩存式文件上傳
            /// </summary>
            /// <param name=""></param>
            /// <returns></returns>
            [HttpPost("UploadingFormFile")]
            public async Task<IActionResult> UploadingFormFile(IFormFile file)
            {
                using (var stream = file.OpenReadStream())
                {
                    var trustedFileNameForFileStorage = Path.GetRandomFileName();
                    await WriteFileAsync(stream, Path.Combine(_targetFilePath, trustedFileNameForFileStorage));
                }
                return Created(nameof(FileController), null);
            }
     
     
            /// <summary>
            /// 寫文件導到磁盤
            /// </summary>
            /// <param name="stream"></param>
            /// <param name="path">文件保存路徑</param>
            /// <returns></returns>
            public static async Task<int> WriteFileAsync(System.IO.Stream stream, string path)
            {
                const int FILE_WRITE_SIZE = 84975;//寫出緩衝區大小
                int writeCount = 0;
                using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write, FILE_WRITE_SIZE, true))
                {
                    byte[] byteArr = new byte[FILE_WRITE_SIZE];
                    int readCount = 0;
                    while ((readCount = await stream.ReadAsync(byteArr, 0, byteArr.Length)) > 0)
                    {
                        await fileStream.WriteAsync(byteArr, 0, readCount);
                        writeCount += readCount;
                    }
                }
                return writeCount;
            }
     
        }
    }

     

     總結:

    如果你部署 在iis上或者Nginx 等其他應用服務器 也是需要注意的事情,因為他們本身也有對請求體的限制,還有值得注意的就是我們在創建文件流對象時 緩衝區的大小盡量不要超過netcore大對象的限制。這樣在併發高的時候很容易觸發二代GC的回收.

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

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

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

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

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

  • 【Stream—5】MemoryStream相關知識分享

    【Stream—5】MemoryStream相關知識分享

    一、簡單介紹一下MemoryStream

    MemoryStream是內存流,為系統內存提供讀寫操作,由於MemoryStream是通過無符號字節數組組成的,可以說MemoryStream的性能可以算比較出色,所以它擔當起了一些其他流進行數據交互安時的中間工作,同時可降低應用程序中對臨時緩衝區和臨時文件的需求,其實MemoryStream的重要性不亞於FileStream,在很多場合,我們必須使用它來提高性能

    二、MemoryStream和FileStream的區別

    前文中也提到了,FileStream主要對文件的一系列操作,屬於比較高層的操作,但是MemoryStream卻很不一樣,他更趨向於底層內存的操作,這樣能夠達到更快速度和性能,也是他們的根本區別,很多時候,操作文件都需要MemoryStream來實際進行讀寫,最後放入相應的FileStream中,不僅如此,在諸如XmlWriter的操作中也需要使用MemoryStream提高讀寫速度

    三、分析MemoryStream最常見的OutOfMemory異常

    先看一下下面一段簡單的代碼

     1             //測試byte數組 假設該數組容量是256M
     2             var testBytes = new byte[256 * 1024 * 1024];
     3             var ms = new MemoryStream();
     4             using (ms)
     5             {
     6                 for (int i = 0; i < 1000; i++)
     7                 {
     8                     try
     9                     {
    10                         ms.Write(testBytes, 0, testBytes.Length);
    11                     }
    12                     catch
    13                     {
    14                         Console.WriteLine("該內存流已經使用了{0}M容量的內存,該內存流最大容量為{1}M,溢出時容量為{2}M",
    15                             GC.GetTotalMemory(false) / (1024 * 1024),//MemoryStream已經消耗內存量
    16                             ms.Capacity / (1024 * 1024), //MemoryStream最大的可用容量
    17                             ms.Length / (1024 * 1024));//MemoryStream當前流的長度(容量)
    18                         break;
    19                     }
    20                 }
    21             }
    22             Console.ReadLine();

    輸出結果:

     

     

     從輸出結果來看,MemoryStream默認可用最大容量是1024M,發生異常時正好是其最大容量。

    問題來了,假設我們需要操作比較大的文件,該怎麼辦呢?其實有2種方法可以搞定,一種是分段處理,我們將Byte數組分成等份進行處理,還有一種方式便是增加MomoryStream的最大可用容量(字節),我們可以在聲明MomoryStream的構造函數時利用它的重載版本:MemoryStream(int capacity)

    到底使用哪種方法比較好呢?其實筆者認為具體項目具體分析,前者分段處理的確能夠解決大數據量操作的問題,但是犧牲了性能和時間(多線程暫時不考慮),後者可以得到性能上的優勢,但是其允許最大容量是 int.Max,所以無法給出一個明確的答案,大家在做項目時,按照需求自己定製即可,最關鍵的還是要取到性能和開銷的最佳點位,還有一種更噁心的溢出方式,往往會讓大家抓狂,就是不定時溢出,就是MemoryStream處理的文件可能只有40M或更小時,也會發生OutOfMemory的異常,關於這個問題,終於在老外的一篇文章中得到了解釋,運氣還不錯,可以看看這篇博文:,由於涉及到windows的內存機制,包括內存也,進程的虛擬地址空間等,比較複雜,所以大家看他的文章前,我先和大家簡單的介紹一下頁和進程的虛擬地址:

    內存頁:內存頁分為:文件頁和計算頁

    內存中的文件頁是文件緩存區,即文件型的內存頁,用於存放文件數據的內存頁(也稱永久頁),作用在於讀寫文件時可以減少對磁盤的訪問,如果它的大小設置的太小,會引起系統頻繁的訪問磁盤,增加磁盤I/O,設置太大,會浪費內存資源。

    內存中的計算頁也稱為計算型的內存頁,主要用於存放程序代碼和臨時使用的數據。

    進程的虛擬地址:每一個進程被給予它的非常自由的虛擬地址空間。對於32位的進程,地址空間是4G,因為一個32位指針能夠從0x00000000到0xffffffff之間的任意值,這個範圍允許指針從4294967296個值得一個,覆蓋了一個進程得4G範圍,對於64位進程,地址空間是16eb因為一個64位指針能夠指向18,446,744,073,709,551,616個值中的一個,覆蓋一個進程的16eb範圍,這是十分寬廣的範圍,上述概念都在自windows核心編程這本書,其實這本書對於我們程序員來說很重要,對於內存的操作,本人也是小白。

    四、MemoryStream的構造函數

    1、MemoryStream()

    MemoryStream允許不帶參數的構造

    2、MemoryStream(byte[] byte)

    Byte數組是包含了一定數據的byte數組,這個構造很重要,初學者或者用的不是很多的程序員會忽略這個構造函數導致後面讀取或寫入數據時發現MemoryStream中沒有byte數據,會導致很鬱悶的感覺,大家注意一下就行,有時候也可能無需這樣,因為很多方法返回值已經是MemoryStream了。

    3、MemoryStream(int capacity)

    這個是重中之重,為什麼這麼說呢?我在本文探討關於OutMemory異常中也提到了,如果你想額外提高MemoryStream的吞吐量,也只能靠這個方法提升一定的吞吐量,最多也只能到int.Max,這個方法也是解決OutOfMemory的一個可行方案。

    4、MemoryStream(byte[] byte,bool writeable)

    writeable參數定義該流是否可寫

    5、MemoryStream(byte[] byte,int index,int count)

    index:參數定義從byte數組中的索引index

    count:參數是獲取的數據量的個數

    6、MemoryStream(byte[] byte,int index,int count,bool writeable,bool publiclyVisible)

    publiclyVisible:參數表示true可以啟用GetBuffer方法,它返回無符號字節數組,流從該數組創建,否則為false。(大家一定覺得這個很難理解,別急,下面的方法中我會詳細的講一下這個東西)

    五、MemoryStream的屬性

     Memory的屬性大致都是和其父類很相似,這些功能在我的這篇文章中已經詳細討論過,所以我簡單列舉以下其屬性:

     

     其獨有的屬性:

    Capacity:這個前文其實已經提及,它表示該流的可支配容量(字節),非常重要的一個屬性。

    六、MemoryStream的方法

    對於重寫的方法,這裏不再重複說明,大家可以去看一下

     以下是MemoryStream獨有的方法

    1、virtual byte[] GetBuffer()

    這個方法使用時需要小心,因為這個方法返回無符號字節數組,也就是說,即使我只輸入幾個字符例如“HellowWorld”我們只希望返回11個數據就行,可是這個方法會把整個緩衝區的數據,包括那些已經分配但是實際上沒有用到的字符數據都返回回來了,如果想啟用這個方法那必須使用上面最後一個構造函數,將publiclyVisible屬性設置成true就行,這也是上面那個構造函數的錯用所在。

    2、virtual void WriteTo(Stream stream)

    這個方法的目的其實在本文開始的時候討論性能問題時已經指出,MemoryStream常用起中間流的作用,所以讀寫在處理完后將內存吸入其他流中。

    七、示例:

    1、XmlWriter中使用MemoryStream

     1         public static void UseMemoryStreamInXmlWriter()
     2         {
     3             var ms = new MemoryStream();
     4             using (ms)
     5             {
     6                 //定義一個XmlWriter
     7                 using (XmlWriter writer= XmlWriter.Create(ms))
     8                 {
     9                     //寫入xml頭
    10                     writer.WriteStartDocument(true);
    11                     //寫入一個元素
    12                     writer.WriteStartElement("Content");
    13                     //為這個元素增加一個test屬性
    14                     writer.WriteStartAttribute("test");
    15                     //設置test屬性的值
    16                     writer.WriteValue("萌萌小魔王");
    17                     //釋放緩衝,這裏可以不用釋放,但是在實際項目中可能要考慮部分釋放對性能帶來的提升
    18                     writer.Flush();
    19                     Console.WriteLine($"此時內存使用量為:{GC.GetTotalMemory(false)/1024}KB,該MemoryStream已使用容量為:{Math.Round((double)ms.Length,4)}byte,默認容量為:{ms.Capacity}byte");
    20                     Console.WriteLine($"重新定位前MemoryStream所在的位置是{ms.Position}");
    21                     //將流中所在當前位置往後移動7位,相當於空格
    22                     ms.Seek(7, SeekOrigin.Current);
    23                     Console.WriteLine($"重新定位后MemoryStream所存在的位置是{ms.Position}");
    24                     //如果將流所在的位置設置位如下示的位置,則XML文件會被打亂
    25                     //ms.Position = 0;
    26                     writer.WriteStartElement("Content2");
    27                     writer.WriteStartAttribute("testInner");
    28                     writer.WriteValue("萌萌小魔王2");
    29                     writer.WriteEndElement();
    30                     writer.WriteEndElement();
    31                     //再次釋放
    32                     writer.Flush();
    33                     Console.WriteLine($"此時內存使用量為:{GC.GetTotalMemory(false) / 1024}KB,該MemoryStream已使用容量為:{Math.Round((double)ms.Length, 4)}byte,默認容量為:{ms.Capacity}byte");
    34                     //建立一個FileStream 文件創建目的地是f:\test.xml
    35                     var fs=new FileStream(@"f:\test.xml",FileMode.OpenOrCreate);
    36                     using (fs)
    37                     {
    38                         //將內存流注入FileStream
    39                         ms.WriteTo(fs);
    40                         if (ms.CanWrite)
    41                         {
    42                             //釋放緩衝區
    43                             fs.Flush();
    44                         }
    45                     }
    46                     Console.WriteLine();
    47                 }
    48             }
    49         }

    運行結果:

     

     咱看一下XML文本是什麼樣的?

     

     2、自定義處理圖片的HttpHandler

    有時項目里我們必須將圖片進行一定的操作,例如:水印,下載等,為了方便和管理我們可以自定義一個HttpHandler來負責這些工作

    後台代碼:

     1     public class ImageHandler : IHttpHandler
     2     {
     3         /// <summary>
     4         /// 實現IHttpHandler接口中ProcessRequest方法
     5         /// </summary>
     6         /// <param name="context"></param>
     7         public void ProcessRequest(HttpContext context)
     8         {
     9             context.Response.Clear();
    10             //得到圖片名
    11             var imageName = context.Request["ImageName"] ?? "小魔王";
    12             //得到圖片地址
    13             var stringFilePath = context.Server.MapPath($"~/Image/{imageName}.jpg");
    14             //聲明一個FileStream用來將圖片暫時放入流中
    15             FileStream stream=new FileStream(stringFilePath,FileMode.Open);
    16             using (stream)
    17             {
    18                 //通過GetImageFromStream方法將圖片放入Byte數組中
    19                 var imageBytes = GetImageFromStream(stream, context);
    20                 //上下文確定寫道客戶端時的文件類型
    21                 context.Response.ContentType = "image/jpeg";
    22                 //上下文將imageBytes中的數組寫到前端
    23                 context.Response.BinaryWrite(imageBytes);
    24             }
    25         }
    26 
    27         public bool IsReusable => true;
    28 
    29         /// <summary>
    30         /// 將流中的圖片信息放入byte數組后返回該數組
    31         /// </summary>
    32         /// <param name="stream">文件流</param>
    33         /// <param name="context">上下文</param>
    34         /// <returns></returns>
    35         private byte[] GetImageFromStream(FileStream stream, HttpContext context)
    36         {
    37             //通過Stream到Image
    38             var image = Image.FromStream(stream);
    39             //加上水印
    40             image = SetWaterImage(image, context);
    41             //得到一個ms對象
    42             MemoryStream ms = new MemoryStream();
    43             using (ms)
    44             {
    45                 //將圖片保存至內存流
    46                 image.Save(ms,ImageFormat.Jpeg);
    47                 byte[] imageBytes = new byte[ms.Length];
    48                 ms.Position = 0;
    49                 //通過內存流放到imageBytes
    50                 ms.Read(imageBytes, 0, imageBytes.Length);
    51                 //ms.Close();
    52                 //返回imageBytes
    53                 return imageBytes;
    54             }
    55         }
    56 
    57         private Image SetWaterImage(Image image, HttpContext context)
    58         {
    59             Graphics graphics = Graphics.FromImage(image);
    60             Image waterImage = Image.FromFile(context.Server.MapPath("~/Image/logo.jpg"));
    61             graphics.DrawImage(waterImage, new Point { X = image.Size.Width - waterImage.Size.Width, Y = image.Size.Height - waterImage.Size.Height });
    62             return image;
    63         }
    64     }

    別忘了,還要再web.config中進行配置,如下:

     

     這樣前台就能使用了

     

     讓我們來看一下輸出結果:

     

     哈哈,還不錯。

    好了,MemoryStream相關的知識就先分享到這裏了。同志們,再見!

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

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

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

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

  • fastjason常用方法

    fastjason常用方法

    什麼是fastjson?

    Fastjson是一個Java語言編寫的高性能功能完善的JSON庫。它採用一種“假定有序快速匹配”的算法,把JSON Parse的性能提升到極致,是目前Java語言中最快的JSON庫。Fastjson接口簡單易用,已經被廣泛使用在緩存序列化、協議交互、Web輸出、Android客戶端等多種應用場景。

    主要特點:

    • 快速FAST (比其它任何基於Java的解析器和生成器更快,包括jackson)
    • 強大(支持普通JDK類包括任意Java Bean Class、Collection、Map、Date或enum)
    • 零依賴(沒有依賴其它任何類庫除了JDK)

    背景

    最近關於fastjson的消息,引起了很多人的關注!

    fastjson爆出重大漏洞,攻擊者可使整個業務癱瘓

    漏洞描述

    常用JSON組件FastJson存在遠程代碼執行漏洞,攻擊者可通過精心構建的json報文對目標服務器執行任意命令,從而獲得服務器權限。此次爆發的漏洞為以往漏洞中autoType的繞過。

    影響範圍

    FastJson < 1.2.48

    很多開發者才猛然發現,fastjson已經深入到我們開發工作的方方面面。那麼除了趕快升級你的json外,我們來挖挖fastjson最常用的用法。

    fastjson常用方式

    1.maven依賴(記得升級到1.2.48以上版本哦)

            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
            <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
             <version>1.2.62</version>
            </dependency>    

    2.FastJson對於json格式字符串的解析主要用到了一下三個類:

    (1)JSON:fastJson的解析器,用於JSON格式字符串與JSON對象及javaBean之間的轉換。

    (2)JSONObject:fastJson提供的json對象。

    (3)JSONArray:fastJson提供json數組對象。

    3.常用方式

    3.1 string和java對象

     

    實例1:對象轉json字符串

            Map<String,String> map=new HashMap<String,String>();
            map.put("code","0");
            map.put("message","ok");
            String json=JSON.toJSONString(map);
            System.out.println(json);

    輸出結果為:

    {"code":"0","message":"ok"}

    實例2:字符串轉對象

            Map<String,String> map=new HashMap<String,String>();
            map.put("code","0");
            map.put("message","ok");
            String json=JSON.toJSONString(map);
            System.out.println(json);
            
            Map obj=(Map)JSON.parse(json);
            System.out.println("code="+obj.get("code")+",message="+obj.get("message"));

    輸出結果

    {"code":"0","message":"ok"}
    code=0,message=ok

    3.2 工具類JSONObject

        public static void main(String[] args) {
            Map<String,String> map=new HashMap<String,String>();
            map.put("code","0");
            map.put("message","ok");
            String json=JSON.toJSONString(map);
            System.out.println(json);
            
            Map obj=(Map)JSON.parse(json);
            System.out.println("code="+obj.get("code")+",message="+obj.get("message"));        
            
            String code=JSON.parseObject(json).getString("code");
            String message=JSON.parseObject(json).getString("message");
            System.out.println("code="+code+",message="+message);
        }

    輸出結果

    {"code":"0","message":"ok"}
    code=0,message=ok
    code=0,message=ok

    3.3 數組對象

    List<user> list=new ArrayList<user>(JSONArray.parseArray(jsonString,user.class)); 

    Fastjson 與各種JSON庫的性能比較:

     

    json庫 序列化性能 反序列化性能 jar大小
    fastjson 1201 1216 fastjson-1.1.26.jar(356k)
    fastjson-1.1.25-android.jar(226k)
    jackson 1408 1915 jackson-annotations-2.1.1.jar(34k)
    jackson-core-2.1.1.jar(206k)
    jackson-databind-2.1.1.jar(922k)
    總共1162k
    gson 7421 5065 gson-2.2.2.jar(189k)
    json-lib 27555 87292 json-lib-2.4-jdk15.jar(159k)


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

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

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

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

  • Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多數據源

    Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多數據源

    本文是 Spring Boot 整合數據持久化方案的最後一篇,主要和大夥來聊聊 Spring Boot 整合 Jpa 多數據源問題。在 Spring Boot 整合JbdcTemplate 多數據源、Spring Boot 整合 MyBatis 多數據源以及 Spring Boot 整合 Jpa 多數據源這三個知識點中,整合 Jpa 多數據源算是最複雜的一種,也是很多人在配置時最容易出錯的一種。本文大夥就跟着松哥的教程,一步一步整合 Jpa 多數據源。

    工程創建

    首先是創建一個 Spring Boot 工程,創建時添加基本的 Web、Jpa 以及 MySQL 依賴,如下:

    創建完成后,添加 Druid 依賴,這裏和前文的要求一樣,要使用專為 Spring Boot 打造的 Druid,大夥可能發現了,如果整合多數據源一定要使用這個依賴,因為這個依賴中才有 DruidDataSourceBuilder,最後還要記得鎖定數據庫依賴的版本,因為可能大部分人用的還是 5.x 的 MySQL 而不是 8.x。完整依賴如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.28</version>
        <scope>runtime</scope>
    </dependency>

    如此之後,工程就創建成功了。

    基本配置

    在基本配置中,我們首先來配置多數據源基本信息以及 DataSource,首先在 application.properties 中添加如下配置信息:

    #  數據源一
    spring.datasource.one.username=root
    spring.datasource.one.password=root
    spring.datasource.one.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8
    spring.datasource.one.type=com.alibaba.druid.pool.DruidDataSource
    
    #  數據源二
    spring.datasource.two.username=root
    spring.datasource.two.password=root
    spring.datasource.two.url=jdbc:mysql:///test02?useUnicode=true&characterEncoding=UTF-8
    spring.datasource.two.type=com.alibaba.druid.pool.DruidDataSource
    
    # Jpa配置
    spring.jpa.properties.database=mysql
    spring.jpa.properties.show-sql=true
    spring.jpa.properties.database-platform=mysql
    spring.jpa.properties.hibernate.ddl-auto=update
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

    這裏 Jpa 的配置和上文相比 key 中多了 properties,多數據源的配置和前文一致,然後接下來配置兩個 DataSource,如下:

    @Configuration
    public class DataSourceConfig {
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.one")
        @Primary
        DataSource dsOne() {
            return DruidDataSourceBuilder.create().build();
        }
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.two")
        DataSource dsTwo() {
            return DruidDataSourceBuilder.create().build();
        }
    }

    這裏的配置和前文的多數據源配置基本一致,但是注意多了一個在 Spring 中使用較少的註解 @Primary,這個註解一定不能少,否則在項目啟動時會出錯,@Primary 表示當某一個類存在多個實例時,優先使用哪個實例。

    好了,這樣,DataSource 就有了。

    多數據源配置

    接下來配置 Jpa 的基本信息,這裏兩個數據源,我分別在兩個類中來配置,先來看第一個配置:

    @Configuration
    @EnableJpaRepositories(basePackages = "org.javaboy.jpa.dao",entityManagerFactoryRef = "localContainerEntityManagerFactoryBeanOne",transactionManagerRef = "platformTransactionManagerOne")
    public class JpaConfigOne {
        @Autowired
        @Qualifier(value = "dsOne")
        DataSource dsOne;
        @Autowired
        JpaProperties jpaProperties;
        @Bean
        @Primary
        LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBeanOne(EntityManagerFactoryBuilder builder) {
            return builder.dataSource(dsOne)
                    .packages("org.javaboy.jpa.model")
                    .properties(jpaProperties.getProperties())
                    .persistenceUnit("pu1")
                    .build();
        }
        @Bean
        PlatformTransactionManager platformTransactionManagerOne(EntityManagerFactoryBuilder builder) {
            LocalContainerEntityManagerFactoryBean factoryBeanOne = localContainerEntityManagerFactoryBeanOne(builder);
            return new JpaTransactionManager(factoryBeanOne.getObject());
        }
    }

    首先這裏注入 dsOne,再注入 JpaProperties,JpaProperties 是系統提供的一個實例,裡邊的數據就是我們在 application.properties 中配置的 jpa 相關的配置。然後我們提供兩個 Bean,分別是 LocalContainerEntityManagerFactoryBean 和 PlatformTransactionManager 事務管理器,不同於 MyBatis 和 JdbcTemplate,在 Jpa 中,事務一定要配置。在提供 LocalContainerEntityManagerFactoryBean 的時候,需要指定 packages,這裏的 packages 指定的包就是這個數據源對應的實體類所在的位置,另外在這裏配置類上通過 @EnableJpaRepositories 註解指定 dao 所在的位置,以及 LocalContainerEntityManagerFactoryBean 和 PlatformTransactionManager 分別對應的引用的名字。

    好了,這樣第一個就配置好了,第二個基本和這個類似,主要有幾個不同點:

    • dao 的位置不同
    • persistenceUnit 不同
    • 相關 bean 的名稱不同

    注意實體類可以共用。

    代碼如下:

    @Configuration
    @EnableJpaRepositories(basePackages = "org.javaboy.jpa.dao2",entityManagerFactoryRef = "localContainerEntityManagerFactoryBeanTwo",transactionManagerRef = "platformTransactionManagerTwo")
    public class JpaConfigTwo {
        @Autowired
        @Qualifier(value = "dsTwo")
        DataSource dsTwo;
        @Autowired
        JpaProperties jpaProperties;
        @Bean
        LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBeanTwo(EntityManagerFactoryBuilder builder) {
            return builder.dataSource(dsTwo)
                    .packages("org.javaboy.jpa.model")
                    .properties(jpaProperties.getProperties())
                    .persistenceUnit("pu2")
                    .build();
        }
        @Bean
        PlatformTransactionManager platformTransactionManagerTwo(EntityManagerFactoryBuilder builder) {
            LocalContainerEntityManagerFactoryBean factoryBeanTwo = localContainerEntityManagerFactoryBeanTwo(builder);
            return new JpaTransactionManager(factoryBeanTwo.getObject());
        }
    }

    接下來,在對應位置分別提供相關的實體類和 dao 即可,數據源一的 dao 如下:

    package org.javaboy.jpa.dao;
    public interface UserDao extends JpaRepository<User,Integer> {
        List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
        @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
        User maxIdUser();
    }

    數據源二的 dao 如下:

    package org.javaboy.jpa.dao2;
    public interface UserDao2 extends JpaRepository<User,Integer> {
        List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
    
        @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
        User maxIdUser();
    }

    共同的實體類如下:

    package org.javaboy.jpa.model;
    @Entity(name = "t_user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String username;
        private String address;
        //省略getter/setter
    }

    到此,所有的配置就算完成了,接下來就可以在 Service 中注入不同的 UserDao,不同的 UserDao 操作不同的數據源。

    其實整合 Jpa 多數據源也不算難,就是有幾個細節問題,這些細節問題解決,其實前面介紹的其他多數據源整個都差不多。

    好了,本文就先介紹到這裏。

    相關案例已經上傳到 GitHub,歡迎小夥伴們們下載:

    掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 免費 Java 學習乾貨

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

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

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

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

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

  • CSS:CSS彈性盒子布局 Flexible Box

    CSS:CSS彈性盒子布局 Flexible Box

    一、簡介

    flexbox:全稱Flexible Box, 彈性盒子布局。可以簡單實現各種伸縮性的設計,它是由伸縮容器和伸縮項目組成。任何一個元素都可以指定為flexbox布局。這種新的布局方案在2009年是由W3C組織提出來的,在此之前,Web開發一般使用基於盒子模型的傳統頁面布局,依賴定位屬性、流動屬性和显示屬性來解決,參看鏈接:。彈性盒子布局的出現,極大的方便了開發者,在如今的ReactNative開發中,也已經被引入使用。

    伸縮流布局結構圖如下:

    彈性盒子布局具備的特徵:

    1、伸縮容器的子元素稱為伸縮項目,伸縮項目使用伸縮布局來排版。伸縮布局和傳統布局不一樣,它按照伸縮流方向布局。

    2、伸縮容器由兩條軸構成,分別為主軸(main axis)和交叉軸(cross axis)。主軸既可以用水平軸,也可以是豎直軸,根據開發者需要來決定。

    3、主軸的起點叫main start,終點叫main end,主軸的空間用main size表示。

    4、交叉軸的起點叫cross start,終點叫cross end,交叉軸的空間用cross size表示。

    5、默認情況下,伸縮項目總是沿着主軸方向排版,從開始位置到終點位置。至於換行显示,則通過設置伸縮屬性來實現。

    6、伸縮容器的屬性有:display、flex-direction、flex-wrap、flex-flow、justify-content、align-items、align-content

    7、伸縮項目的屬性有: order、flex-grow、flex-shrink、flex-basis、flex、align-self

     

    二、伸縮容器的屬性,全局設置排版

    HTML:[注意:下面的演示截圖項目個數會根據需要選擇性註釋“flex-item”,有時用不到5個]

    <!DOCTYPE html>
    <html>
    <head>
        <title>Flexbox</title>
        <!--  採用外聯方式導入css文件 -->
        <link rel="stylesheet" type="text/css" href="./css_test.css">
    </head>
    <body>
        <span class="flex-container"> 
            <span class="flex-item" id="item1" style="color:white;font-size:20px">1</span>
            <span class="flex-item" id="item2" style="color:white;font-size:20px">2</span>
            <span class="flex-item" id="item3" style="color:white;font-size:20px">3</span>
            <span class="flex-item" id="item4" style="color:white;font-size:20px">4</span>
            <span class="flex-item" id="item5" style="color:white;font-size:20px">5</span>
        </span>
    </body>
    </html> 

    1、display:決定元素是否為伸縮容器

    • flex:產生塊級伸縮容器
      .flex-container {
           display: flex;
       }
    • inline-flex:產生行內塊級伸縮容器
    •  .flex-container {
           display: inline-flex;
       }

    2、flex-direction:指定伸縮容器主軸的方向

    • row:水平方向,從左到右
       .flex-container {
           display: flex;
           flex-direction: row;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; 
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • row-reverse:水平方向,從右到左
       .flex-container {
           display: flex;
           flex-direction: row-reverse;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; 
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • column:豎直方向,從上到下
       .flex-container {
           display: flex;
           flex-direction: column;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px; 
           background-color: green;
           margin: 1px;
       }

    • column-reverse:豎直方向,從下到上
       .flex-container {
           display: flex;
           flex-direction: column-reverse;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px; 
           background-color: green;
           margin: 1px;
       }

    3、flex-wrap:指定伸縮容器主軸方向空間不足時,決定是否換行以及換行方式

    • nowarp:不換行
      .flex-container {
           display: flex;
           flex-direction: row;
           flex-wrap: nowrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; //下圖單行狀態寬度被重新計算
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • warp:換行,若主軸為水平方向,換行方向是從上到下
       .flex-container {
           display: flex;
           flex-direction: row;
           flex-wrap: wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • wrap-reverse:換行,若主軸為水平方向,換行方向是從下到上
       .flex-container {
           display: flex;
           flex-direction: row;
           flex-wrap: wrap-reverse;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    4、flex-flow:flex-direction和flex-wrap的縮寫,同時指定伸縮容器主軸方向和換行設置

    • row nowrap:默認主軸是水平方向,從左到右,且不換行
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; //下圖單行狀態寬度被重新計算
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    5、justify-content:決定伸縮項目沿着主軸線的對齊方式

    • flex-start:與主軸線起始位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           justify-content: flex-start;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • flex-end:與主軸線結束位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           justify-content: flex-end;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • center:與主軸線中間位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           justify-content: center;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • space-between:平均分配到主軸線里,第一個項目靠齊起始位置,最後一個項目靠齊終點位置
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           justify-content: space-between;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • sapce-around:平均分配到主軸線里,兩端保留一半的空間
      .flex-container {
           display: flex;
           flex-flow: row wrap;
           justify-content: space-around;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    6、align-items:決定伸縮項目不能換行時沿着交叉軸線的對齊方式

    • flex-start:與交叉軸線起始位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           align-items: flex-start;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; //下圖單行狀態寬度被重新計算
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • flex-end:與交叉軸線結束位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           align-items: flex-end;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; //下圖單行狀態寬度被重新計算
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • center:與交叉軸線中間位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           align-items: center;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • baseline:根據基線對齊
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           align-items: baseline;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item1 {
           padding-top: 25px;
       }
      
       #item2 {
           padding-top: 20px;
       }
        
       #item3 {
           padding-top: 15px;
       }
      
       #item4 {
           padding-top: 10px;
       }
        
       #item5 {
           padding-top: 5px;
       }

    • stretch:沿着交叉軸線拉伸填充整個伸縮容器
       .flex-container {
           display: flex;
           flex-flow: row nowrap;
           align-items: stretch;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;//此時可以設置寬度,但不能設置高度,否則無法拉伸
           background-color: green;
           margin: 1px;
       }

    7、align-content:決定伸縮項目可以換行時沿着交叉軸線的對齊方式,flex-warp:warp一定要開啟

    • flex-start:與交叉軸線起始位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:flex-start;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • flex-end:與交叉軸線結束位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:flex-end;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • center:與主軸線中間位置靠齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:center;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • space-between:平均分配到主軸線里,第一行項目靠齊起始位置,最後一行項目靠齊終點位置
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:space-between;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • sapce-around:所有行平均分配到主軸線里,兩端保留一半的空間
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:space-around;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }

    • stretch:沿着交叉軸
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           align-content:stretch;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px; //不要設置高度,不然無法拉伸
           background-color: green;
           margin: 1px;
       }

     

    三、伸縮項目的屬性,單個設置排版

    1、order:定義伸縮項目的排列順序。數值越小,排列越靠前,默認值為0。

    • 表達式 order: integer;
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item4 {
           order: -1;
       }
      
       #item5 {
           order: -2;
       }

    2、flex-grow:定義伸縮項目的放大比例,默認值為0,表示即使存在剩餘空間,也不放大。

    • 表達式 flex-grow: number;
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item2 {
           flex-grow: 1; //空間不足,item2不會放大
       }
      
       #item4 {
           flex-grow: 1; //item4放大填滿剩餘空間
       }

    3、flex-shrink:定義伸縮項目的收縮比例,默認值為1。

    • 表達式 flex-shrink: numer;
      .flex-container {
           display: flex;
           flex-flow: row nowrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item4 {
           flex-shrink:3; //單行,空間有限,item4縮小為原來的1/3
       }
      
       #item5 {
           flex-shrink:4;  //單行,空間有限,item5縮小為原來的1/5
      }

    4、flex-basis:定義伸縮項目的基準值,剩餘空間按照比例進行伸縮,默認auto。

    •  auto
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item5 {
           flex-basis:auto;
       }

                

    • flex-basis: length 
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item5 {
           flex-basis:200px;
       }

    5、flex:是flex-grow、flex-shrink、flex-basis的縮寫,默認值 0 1 auto。

    • none: 不設置
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item2 {
           flex: none; /* 等同於 flex: 0 0 auto */
       }

    • flex-grow flex-shrink flex-basis: 設置放大或縮小或基準線
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item2 {
           flex: 1; /* 等同於 flex: 1 1 auto 或者 等同於 flex: auto*/
       }

    6、align-self:用來設置伸縮項目在交叉軸的對齊方式。

    • auto:自動對齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item3 {
           align-self: auto;
       }

    • flex-start: 向交叉軸的開始位置對齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item3 {
           align-self: flex-start;
       }

    • flex-end: 向交叉軸的結束位置對齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item3 {
           align-self: flex-end;
       }

    • center: 向交叉軸的中間位置對齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item3 {
           align-self: center;
       }

    • baseline:向交叉軸的基準線對齊
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           height: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item1 {
           align-self: baseline;
           margin-top: 50px;
       }
      
       #item2 {
           align-self: baseline;
       }

    • stretch: 在交叉軸拉伸填滿伸縮容器
       .flex-container {
           display: flex;
           flex-flow: row wrap;
           width: 160px;
           height: 160px;
           background-color: red;
       }
      
       .flex-item {
           width: 50px;
           background-color: green;
           margin: 1px;
       }
      
       #item1 {
           align-self: stretch;
       }
      
       #item2 {
           align-self: stretch;
       }
      
       #item3 {
           align-self: stretch;
       }

     

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

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

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

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

  • 電動車電池成長,帶動致茂營收倍翻

    受惠於電動車動力電池需求增加,致茂電子的七月合併營收創下歷史新高。今年前七個月的累計營收更已與去年全年相當。

    致茂電子表示,因電動車動力電池製造的關鍵技術(turnkey solutions)銷售蓬勃,帶動七月營收大漲,合併營收達新台幣16.2億元,不僅較上月成長71%、更較去年七月成長101%,營收數字創下歷史新高。

    此外,母公司的7月單月營收也有128%的月增與172%的年增,達新台幣12.6億元。強勁的需求使致茂今年前七個月的合併營收年增27%來到69.1億新台幣;母公司前七個月的累計營收新台幣45億元,也已相當於去年全年水準。

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

    【其他文章推薦】

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

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

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

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

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

  • 傳蘋果電動汽車將採用韓國公司的電池技術

    傳蘋果電動汽車將採用韓國公司的電池技術

    根據行業消息,蘋果近期與一家韓國電池開發商簽署了保密協議,聯合為代號為“泰坦”的汽車專案開發電池。從今年初開始,他們一直在韓國做行政工作。一名蘋果員工一直在這家韓國公司進行參觀活動,他屬於與蘋果電動汽車電池開發相關的部門。  
      業界認為,這家韓國公司並不是唯一一家負責蘋果電池開發的公司。不過,有消息稱,儘管蘋果從一開始就從完全不同的設計、功能以及性能角度來開發電池,但是他們仍舊一直在挖掘創新技術。業界相信,蘋果專注于開發出只能存在於蘋果自動駕駛汽車的創新電池技術。   這家韓國電池開發商由大約20名電池專家組成,持有空芯電池的國際專利技術。這些電池是圓柱形鋰離子二次電池,有兩根手指那麼厚,不同於其它空芯電池。蘋果並未選擇當前電動汽車普遍使用的標準圓形或矩形電池,但計畫根據韓國公司的空芯電池技術為其電動汽車開發自主電池。   文章來源:鳳凰科技

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

    【其他文章推薦】

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

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

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

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