可以寫入教科書的設計 自主品牌又要憋大招了

十來萬的家轎 為什麼需要超低風阻。儘管SUV熱銷,市場佔有率也攀升到36%,但是中國市場依然是青睞轎車的,畢竟轎車還貢獻着53%的乘用車銷量,所以上汽在綜合了爆款SUV RX5及高品質家轎360的能量之上,即將推出一款准B級轎車,目前定名為i6,作為榮威品牌旗下的全新轎車產品序列。

隨着爆款SUV榮威RX5的熱銷,在後台與讀者聊天過程中又有了越來越多坐等提RX5的車主。等提車的過程是難受的,也深有體會。日前,就到了上汽安亭工廠,給大家去扒一扒近期上汽乘用車的狀況。

RX5月銷2萬 產能攀升至月產3萬

早在榮威RX5上市前,其實已經試駕過當中的20T和30T的車型,只是由於微信不太方便查找,很多車友可能會沒看到,以至於依然有不少新加上的同學們還在後台問這款車怎麼樣。下面先來個回顧:

上山劈彎 妹紙在後排葛優躺 這款十來萬的SUV真有那麼舒服?

試駕完這款20T的SUV,只有6個字評價…

這款由中國最大的車企和最大的互聯網公司共同打造的SUV能成為爆款,覺得一點都不意外。如今上市僅3個月的榮威RX5月銷量已經攀升至2萬台,目前還有大量的訂單還等待工廠排產,從廠方得來的消息,目前榮威RX5的產能在一直擴大,上海臨港加上南京浦口的工廠在雙線生產中,產能攀升到41JpH,即每小時能生產41台,估摸下來每月能生產3萬台,就在前天11月9日就迎來了第5萬台的下線。也就是說,各位正在等待提車的准RX5車主們,也不用等很久了。按照這個勢頭,11月可以輕易做到2萬5千台的銷量,衝進月銷前5也不是什麼難事。

說好的插電混動eRX5什麼時候上市?

北京車展上與RX5一同亮相的還有eRX5,這是一款插電式混合動力的SUV,大嘴格柵的設計比RX5更為大氣。目前市面上熱銷的混動SUV真不多,不插電的主要有雷克薩斯NX和RX,以及豐田明年可能會上榮放及漢蘭達的混動版(雙擎),插電混動的目前有沃爾沃XC90 T8,以及比亞迪唐宋元。除此以外,並沒有更多的選擇。也正期待着,加上混合動力這項上牌優勢的eRX5的市場表現如何。

上汽在新能源上也非常有建樹,不僅新能源轎車e550、e950獲得銷量領先。就在上月的10月28日,中國汽車工業科學技術獎揭曉,上汽新能源憑藉在EDU電驅、HCU電控和ESS電池方面的創新,成為今年插電混動領域唯一獲一等獎的車企,這也是上汽時隔5年再次獲得中國汽車界最高榮譽。

據本次上汽工廠行的了解,eRX5也將在11月18日在廣州車展上宣布上市。外形上,主要與RX5是只有前格柵、霧燈、輪轂等明顯的區別,內飾依然是大面積的軟包,新增了12.3英寸的全尺寸液晶儀錶盤。顏色配置新增了更能體現高級感的銀恭弘=叶 恭弘金漆面及松露棕內飾,見過實車之後確實感覺比RX5更上檔次。動力方面,採用與RX5 20T一樣的1.5TGI發動機,配合電驅單元並匹配EDU智能電驅變速箱,可提供最大704牛米的峰值扭矩,純電續航里程為60KM,綜合百公里油耗1.6L。

十來萬的家轎 為什麼需要超低風阻?

儘管SUV熱銷,市場佔有率也攀升到36%,但是中國市場依然是青睞轎車的,畢竟轎車還貢獻着53%的乘用車銷量,所以上汽在綜合了爆款SUV RX5及高品質家轎360的能量之上,即將推出一款准B級轎車,目前定名為i6,作為榮威品牌旗下的全新轎車產品序列。

作為律動設計的原點–上汽榮威Vision-R概念車,相信很多讀者都對其印象深刻,榮威i6沿用榮威Vision-R的設計理念,可以看作是最接近Vision-R概念車的量產車型。i6的空氣動力學設計簡直可以收到國產車教科書中作為案例,其有着0.25cd的超低風阻係數。0.25cd是什麼概念?列舉一系列車型對比下:

CLA

E-CLASS

A4

Model S

榮威 i6

威朗轎跑

凌渡

普通家用轎車領域,近年來奔馳的空力設計是最出色的,而轎車的風阻係數越低,就意味着越省油,行業內同行的說法是風阻係數每降低10%,油耗能降低3%。榮威i6由於大量沿用了概念車的設計,加上上汽工程師團隊進行了大量的試驗調整,精心處理每一處細節,使得i6能實現0.25的超低風阻係數。在主流轎車當中,可以排得上前五名,是上汽多年技術積累的一次實力表現。

i6的車身尺寸為4671*1835*1464毫米,軸距2715毫米,整體車身比例以“優雅寬體比例”為基礎,擁有同級別最寬的車身,達到1835mm,無論外觀和內在都符合榮威律動設計當中橫向舒展的理念。長達2715毫米的軸距同時也造就了同級最大的後排空間。內飾及配置將會與RX5一致,同樣是大面積的軟包,加上10.4寸搭配YunOS的斑馬智行互聯網車載系統。

新車將搭載上汽的藍芯、綠芯系列動力總成,即包括了傳統的燃油、混合、純電動力系統。燃油發動機方面,將會有1.0T及1.5T的配置。全新的1.0TGI,融合了目前先進的技術,包括缸內中置直噴、低慣量渦輪,採用全鋁合金打造,排量1.0L,最大功率92Kw/125匹馬力,峰值扭矩170牛•米,動力性能優於1.6L四缸發動機,整機採用缸蓋機體裙架曲軸連桿設計,以高度輕量化降低整車油耗和排放,提升動力。

據目前最新的消息,榮威i6將會在廣州車展上亮相,屆時大家便可以一睹這款真正屬於“互聯網”時代全新轎車的真容。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

ASP.NET Core Blazor Webassembly 之 路由

web最精妙的設計就是通過url把多個頁面串聯起來,並且可以互相跳轉。我們開發系統的時候總是需要使用路由來實現頁面間的跳轉。傳統的web開發主要是使用a標籤或者是服務端redirect來跳轉。那今天來看看Blazor是如何進行路由的。

使用@page指定組件的路由path

我們可以在Blazor里給每個組件指定一個path,當路由匹配的時候會显示這個組件。

@page "/page/a"

 <h2>
     PAGE A
 </h2>

@code {
 
}

訪問/page/a 看到Page A頁面被渲染出來了。

注意:如果是在瀏覽器里敲入url按回車切換頁面,會發生一次http請求,然後重新渲染blazor應用。

使用a標籤進行頁面跳轉

a標籤作為超鏈接是我們web開發最常用的跳轉方式,blazor同樣支持。
新建Page B

@page "/page/b"

 <h2>
     PAGE B
 </h2>

@code {
 
}

在Page A頁面添加一個a標籤進行跳轉:

@page "/page/a"

 <h2>
     PAGE A
 </h2>
<p>
    <a href="/page/b">Page B</a>
</p>

@code {
 
}

運行一下試試:

注意:使用a連接在頁面間進行跳轉不會發生http請求到後台,頁面是直接在前端渲染出來的。

通過路由傳參

通過http的url進行頁面間傳參是我們web開發的常規操作。下面我們演示下如何從Page A傳遞一個參數到Page B。我們預設Page A裏面有個UserName需要傳遞到Page B,並且显示出來。

通過path傳參

通過url傳參一般有兩種方式,一種是直接把參數組合在path里,比如“/page/b/小明”這樣。

修改Page A:

@page "/page/a"

 <h2>
     PAGE A
 </h2>
<p>
    <a href="/page/b/@userName">Page B</a>
</p>

@code {
    private string userName = "小明";
}

通過把userName組合到path上傳遞給Page B。

修改Page B:

@page "/page/b/{userName}"

 <h2>
     PAGE B
 </h2>
<p>
    userName: @userName
</p>

@code {
    [Parameter]
    public string userName { get; set; }
}

Page B 使用一個“/page/b/{userName}” pattern來匹配userName,並且userName需要標記[Parameter]並且設置為public。

通過QueryString傳參

除了把參數直接拼接在path里,我們還習慣通過QueryString方式傳遞,比如“/page/b?username=小明”。

修改Page A:

@page "/page/a"

<h2>
    PAGE A
</h2>
<p>
    <a href="/page/b?username=@userName">Page B</a>
</p>

@code {
    private string userName = "小明";
}

首先安裝一個工具庫:

Install-Package Microsoft.AspNetCore.WebUtilities -Version 2.2.0

修改Page B:

@page "/page/b"

<h2>
    PAGE B
</h2>
<p>
    userName: @UserName
</p>

@using Microsoft.AspNetCore.WebUtilities;

@inject NavigationManager NavigationManager;

@code {
    [Parameter]
    public string UserName { get; set; }


    protected override void OnInitialized()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
        QueryHelpers.ParseQuery(uri.Query).TryGetValue("username", out Microsoft.Extensions.Primitives.StringValues userName);
        Console.WriteLine(NavigationManager.Uri);
        UserName = userName.ToString();
        Console.WriteLine(UserName);

        base.OnInitialized();
    }
}

頁面獲取QueryString的傳參比較麻煩,Blazor並沒有進行封裝。所以我們需要通過QueryHelpers.ParseQuery方法手工把QueryString格式化成字典形式,然後獲取對應的參數。QueryHelpers類存在Microsoft.AspNetCore.WebUtilities這個庫里,需要通過nuget安裝。

NavLink

NavLink是個導航組件,它其實就是封裝了a標籤。當選中的時候,也就是當前的url跟它的href一致的時候,會自動在class上加上active類,所以可以用來控制選中的樣式。默認的3個導航菜單就是用的NavLink。

比如導航到counter的NavLink:

   <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
    </NavLink>

最後翻譯成html:

<a href="counter" class="nav-link active">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
</a>

NavigationManager

有的時候我們可能需要在代碼里進行導航,如果是JavaScript我們會用window.location來切換頁面,Blazor為我們提供了相應的封裝:NavigationManager。使用NavigationManager可以通過代碼直接進行頁面間的跳轉。我們在Page A頁面放個按鈕然後通過按鈕的點擊事件進行跳轉:

@page "/page/a"

<h2>
    PAGE A
</h2>
<p>
   <button @onclick="GoToB">
       go to B
   </button>
</p>

@inject NavigationManager NavigationManager
@code {

    private void GoToB()
    {
        NavigationManager.NavigateTo("/page/b?username=小貓");
    }

}

修改Page A的代碼,注入NavigationManager對象,通過NavigationManager.NavigateTo方法進行跳轉。

擴展Back方法

Blazor封裝的NavigationManager咋一看以為跟WPF的NavigationService一樣,我想當然的覺得裏面肯定有個Back方法可以進行後退。但是查了一番發現還真的沒有,這就比較尷尬了,沒辦法只能使用JavaScript來實現了。

為了方便我們給NavigationManager直接寫個擴展方法吧。
首先修改Program把IServiceCollection暴露出來:

    public class Program
    {
        public static IServiceCollection Services;

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

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

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

擴展類:

  public static class Ext
    {
        public static void Back(this NavigationManager navigation)
        {
            var jsruntime = Program.Services.BuildServiceProvider().GetService<IJSRuntime>();
            jsruntime.InvokeVoidAsync("history.back");
        }
    }

這個擴展方法很簡單,從DI容器里獲取IJSRuntime的實例對象,通過它去調用JavaScript的history.back方法。

修改Page B:

@page "/page/b"

<h2>
    PAGE B
</h2>
<p>
    userName: @UserName
</p>
<p>
    <button @onclick="GoBack">
        Go back
    </button>
</p>

@using Microsoft.AspNetCore.WebUtilities;

@inject NavigationManager NavigationManager;

@code {
    [Parameter]
    public string UserName { get; set; }


    protected override void OnInitialized()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
        QueryHelpers.ParseQuery(uri.Query).TryGetValue("username", out Microsoft.Extensions.Primitives.StringValues userName);
        Console.WriteLine(NavigationManager.Uri);
        UserName = userName.ToString();
        Console.WriteLine(UserName);

        base.OnInitialized();
    }

    private void GoBack()
    {
        NavigationManager.Back();
    }
}

在Page B頁面上添加一個按鈕,點擊調用NavigationManager.Back方法就能回到上一頁。

總結

到此Blazor路由的內容學習的差不多了,整體上沒有什麼特別的,就是NavigationManager只有前進方法沒有後退是比較讓我震驚的。

相關內容:

ASP.NET Core Blazor Webassembly 之 數據綁定
ASP.NET Core Blazor Webassembly 之 組件
ASP.NET Core Blazor 初探之 Blazor WebAssembly
ASP.NET Core Blazor 初探之 Blazor Server

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

【其他文章推薦】

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

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

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

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

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

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

Elasticsearch系列—生產集群部署(下)

概要

本篇繼續講解Elasticsearch集群部署的細節問題

集群重啟問題

如果我們的Elasticsearch集群做了一些離線的維護操作時,如擴容磁盤,升級版本等,需要對集群進行啟動,節點數較多時,從第一個節點開始啟動,到最後一個節點啟動完成,耗時可能較長,有時候還可能出現某幾個節點因故障無法啟動,排查問題、修復故障后才能加入到集群中,此時集群會幹什麼呢?

假設10個節點的集群,每個節點有1個shard,升級后重啟節點,結果有3台節點因故障未能啟動,需要耗費時間排查故障,如下圖所示:

整個過程步驟如下:

  1. 集群已完成master選舉(node6),master發現未加入集群的node1、node2、node3包含的shard丟失,便立即發出shard恢復的指令。

  2. 在線的7台node,將其中一個replica shard升級為primary shard,並且進行為這些primary shard複製足夠的replica shard。

  3. 執行shard rebalance操作。

  4. 故障的3台節點已排除,啟動成功后加入集群。

  5. 這3台節點發現自己的shard已經在集群中的其他節點上了,便刪除本地的shard數據。

  6. master發現新的3台node沒有shard數據,重新執行一次shard rebalance操作。

這個過程可以發現,多做了四次IO操作,shard複製,shard首次移動,shard本地刪除,shard再次移動,這樣憑空造成大量的IO壓力,如果數據量是TB級別的,那費時費力不討好。

出現此類問題的原因是節點啟動的間隔時間不能確定,並且節點越多,這個問題越容易出現,如果可以設置集群等待多少個節點啟動后,再決定是否對shard進行移動,這樣IO壓力就能小很多。

針對這個問題,我們有下面幾個參數:

  • gateway.recover_after_nodes:集群必須要有多少個節點時,才開始做shard恢復操作。
  • gateway.expected_nodes: 集群應該有多少個節點
  • gateway.recover_after_time: 集群啟動后等待的shard恢復時間

如上面的案例,我們可以這樣設置:

gateway.recover_after_nodes: 8
gateway.expected_nodes: 10
gateway.recover_after_time: 5m

這三個參數的含義:集群總共有10個節點,必須要有8個節點加入集群時,才允許執行shard恢復操作,如果10個節點未全部啟動成功,最長的等待時間為5分鐘。

這幾個參數的值可以根據實際的集群規模來設置,並且只能在elasticsearch.yml文件里設置,沒有動態修改的入口。

上面的參數設置合理的情況,集群啟動是沒有shard移動的現象,這樣集群啟動的時候就可以由之前的幾小時,變成幾秒鐘。

JVM和Thread Pool設置

一提到JVM的調優,大家都有手癢的感覺,好幾百個JVM參數,說不定開啟了正確的按鈕,從此ES踏上高性能、高吞吐量的道路。現實情況可能是我們想多了,ES的大部分參數是經過反覆論證的,基本上不用咱們太操心。

JVM GC

Elasticsearch默認使用的垃圾回收器是CMS。

## GC configuration
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

CMS回收器是併發式的回收器,能夠跟應用程序工作線程併發工作,最大程度減少垃圾回收時的服務停頓時間。

CMS還是會有兩個停頓階段,同時在回收特別大的heap時也會有一些問題。儘管有一些缺點,但是CMS對於要求低延時請求響應的軟件來說,還是最佳的垃圾回收器,因此官方的推薦就是使用CMS垃圾回收器。

有一種最新的垃圾回收器叫做G1。G1回收器可以比CMS提供更少的回收停頓時間,而且能夠這對大heap有更好的回收表現。它會將heap劃分為多個region,然後自動預測哪個region會有最多可以回收的空間。通過回收那些region,就可以最小化停頓時長,而且可以針對大heap進行回收。

聽起來還挺好的,只是G1還是比較年輕的一種垃圾回收器,而且經常會發現一些新的bug,這些bug可能會導致jvm掛掉。穩定起見,暫時不用G1,等G1成熟后ES官方推薦后再用不遲。

線程池

我們開發Java應用系統時,對系統調優的一個常見手段就是調整線程池,但在ES中,默認的threadpool設置是非常合理的,對於所有的threadpool來說,除了搜索的線程池,都是線程數量設置的跟cpu core一樣多的。如果我們有8個cpu core,那麼就可以并行運行8個線程。那麼對於大部分的線程池來說,分配8個線程就是最合理的數量。

搜索會有一個更加大的threadpool,線程數量一般被配置為:cpu core * 3 / 2 + 1。

Elasticsearch的線程池分成兩種:接受請求的線程和處理磁盤IO操作的線程,前面那種由ES管理,后一種由Lucene管理,它們之間會進行協作,ES的線程不會因為IO操作而block住,所以ES的線程設置跟CPU核數一樣或略大於CPU核數即可。

服務器的計算能力是非常有限的,線程池的數量過大會導致上下文頻繁切換,更費資源,如果threadpool大小設置為50,100,甚至500,會導致CPU資源利用率很低,性能反而下降。

只需要記住:用默認的線程池,如果真要修改,以CPU核數為準。

heap內存設置最佳實踐

Elasticsearch默認的jvm heap內存大小是2G,如果是研發環境,我會改成512MB,但在生產環境2GB有點少。

在config/jvm.options文件下,可以看到heap的設置:

# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space

-Xms2g
-Xmx2g

分配規則

Elasticsearch使用內存主要有兩個大戶:jvm heap和lucene,前者ES用來存放很多數據結構來提供更快的操作性能,後者使用os cache緩存索引文件,包括倒排索引、正排索引,os cache內存是否充足,直接影響查詢檢索的性能。

一般的分配規則是:jvm heap佔用小於一半的內存,剩下的全歸lucene使用。

如果單台機器總內存64GB,那麼heap頂格內存分配為32GB,因為32GB內存以下,jvm會使用compressed oops來解決object pointer耗費過大空間的問題,超過32GB后,jvm的compressed oops功能關閉,這樣就只能使用64位的object pointer,會耗費更多的空間,過大的object pointer還會在cpu,main memory和LLC、L1等多級緩存間移動數據的時候,吃掉更多的帶寬。最終的結果可能是50GB內存的效果和32GB一樣,白白浪費了十幾GB內存。

這裏涉及到jvm的object pointer指針壓縮技術,有興趣可以單獨了解一下。

如果單台機器總內存小於64GB,一般heap分配為總內存的一半即可,具體要看預估的數據量是多少。

如果使用超級機器,1TB內存的那種,官網不建議上那麼牛逼的機器,建議分配4-32GB內存給heap,其他的全部用來做os cache,這樣數據量全部緩存在內存中,不落盤查詢,性能杠杠滴。

最佳實踐建議

  1. 將heap的最小值和最大值設置為一樣大。
  2. elasticsearch jvm heap設置得越大,就有越多的內存用來進行緩存,但是過大的jvm heap可能會導致長時間的gc停頓。
  3. jvm heap size的最大值不要超過物理內存的50%,才能給lucene的file system cache留下足夠的內存。
  4. jvm heap size設置不要超過32GB,否則jvm無法啟用compressed oops,將對象指針進行壓縮,確認日誌里有[node-1] heap size [1007.3mb], compressed ordinary object pointers [true] 字樣出現。
  5. 最佳實踐數據:heap size設置的小於zero-based compressed ooops,也就是26GB,但是有時也可以是30GB。通過-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode開啟對應,確認有heap address: 0x00000000e0000000, size: 27648 MB, Compressed Oops mode: 32-bit字樣,而不是heap address: 0x00000000f4000000, size: 28672 MB, Compressed Oops with base: 0x00000000f3ff0000字樣。

swapping問題

部署Elasticsearch的服務盡可能關閉到swap,如果內存緩存到磁盤上,那查詢效率會由微秒級降到毫秒級,會造成性能急劇下降的隱患。

關閉辦法:

  1. Linux系統執行 swapoff -a 關閉swap,或在/etc/fstab文件中配置。

  2. elasticsearch.yml中可以設置:bootstrap.mlockall: true 鎖住自己的內存不被swap到磁盤上。

使用命令 GET _nodes?filter_path=**.mlockall 可以查看是否開啟mlockall
響應信息:

{
  "nodes": {
    "A1s1uus7TpuDSiT4xFLOoQ": {
      "process": {
        "mlockall": true
      }
    }
  }
}

Elasticsearch啟動的幾個問題

  1. root用戶啟動實例的問題
    如果你用root用戶啟動Elasticsearch的實例,將得到如下的錯誤提示:
org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root
	at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:140) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:127) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:124) ~[elasticsearch-cli-6.3.1.jar:6.3.1]
	at org.elasticsearch.cli.Command.main(Command.java:90) ~[elasticsearch-cli-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:93) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:86) ~[elasticsearch-6.3.1.jar:6.3.1]
Caused by: java.lang.RuntimeException: can not run elasticsearch as root
	at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:104) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:171) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:326) ~[elasticsearch-6.3.1.jar:6.3.1]
	at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:136) ~[elasticsearch-6.3.1.jar:6.3.1]
	... 6 more

無它,建立一個用戶,專門用來啟動Elasticsearch的,如esuser,相應的系統目錄和數據存儲目錄都賦予esuser賬戶為歸屬者。

  1. 啟動時提示elasticsearch process is too low,並且無法啟動成功

完整的提示信息:

max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]
memory locking requested for elasticsearch process but memory is not locked

解決辦法:設置系統參數,命令行中的esuser為建立的Linux用戶。

[root@elasticsearch01 bin]# vi /etc/security/limits.conf

# 在文件最後添加
esuser hard nofile 65536
esuser soft nofile 65536
esuser soft memlock unlimited
esuser hard memlock unlimited

設置完成后,可以通過命令查看結果:

# 請求命令
GET _nodes/stats/process?filter_path=**.max_file_descriptors

# 響應結果
{
  "nodes": {
    "A1s1uus7TpuDSiT4xFLOoQ": {
      "process": {
        "max_file_descriptors": 65536
      }
    }
  }
}
  1. 提示vm.max_map_count [65530] is too low錯誤,無法啟動實例

完整的提示信息:

max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

解決辦法:添加vm.max_map_count配置項

臨時設置:sysctl -w vm.max_map_count=262144

永久修改:修改vim /etc/sysctl.conf文件,添加vm.max_map_count設置

[root@elasticsearch01 bin]# vim /etc/sysctl.conf

# 在文件最後添加
vm.max_map_count=262144

# 執行命令
[root@elasticsearch01 bin]# sysctl -p

Elasticsearch實例啟停

實例一般使用後台啟動的方式,在ES的bin目錄下執行命令:

[esuser@elasticsearch01 bin]$ nohup ./elasticsearch &
[1] 15544
[esuser@elasticsearch01 bin]$ nohup: 忽略輸入並把輸出追加到"nohup.out"

這個elasticsearch沒有stop參數,停止時使用kill pid命令。

[esuser@elasticsearch01 bin]$ jps | grep Elasticsearch
15544 Elasticsearch
[esuser@elasticsearch01 bin]$ kill -SIGTERM 15544

發送一個SIGTERM信號給elasticsearch進程,可以優雅的關閉實例。

小結

本篇接着上篇的內容,講解了集群重啟時要注意的問題,JVM Heap設置的最佳實踐,以及Elasticsearch實例啟動時常見的問題解決辦法,最後是Elasticsearch優雅關閉的命令。

專註Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公眾號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信群共同探討技術

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

【其他文章推薦】

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

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

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

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

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

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

我去,這麼簡單的條件表達式竟然也有這麼多坑

最近,小黑哥在一個業務改造中,使用三目運算符重構了業務代碼,沒想到測試的時候竟然發生 NPE 的問題。

重構代碼非常簡單,代碼如下:

// 方法返回參數類型為 Integer
//  private Integer code;
SimpleObj simpleObj = new SimpleObj();
// 其他業務邏輯
if (simpleObj == null) {
    return -1;
} else {
    return simpleObj.getCode();
}

這段 if 判斷,小黑哥看到的時候,感覺很是繁瑣,於是使用條件表達式重構了一把,代碼如下:

// 方法返回參數類型為 Integer
SimpleObj simpleObj = new SimpleObj();
// 其他業務邏輯
return simpleObj == null ? -1 : simpleObj.getCode();

測試的時候,第四行代碼拋出了空指針,這裏代碼很簡單,顯然只有 simpleObj#getCode才有可能發生 NPE 問題。

但是我明明為 simpleObj做過判空判斷,simpleObj 對象肯定不是 null,那麼只有 simpleObj#getCode 返回為 null。但是我的代碼並沒有對這個方法返回值做任何操作,為何會觸發 NPE?

難道是又是自動拆箱導致的 NPE 問題?

在解答這個問題之前,我們首先複習一下條件表達式。

點贊再看,養成習慣。微信搜索『程序通事』,關注查看最新文章~

三目運算符

三目運算符,官方英文名稱:Conditional Operator ? :,又叫條件表達式,本文不糾結名稱,統一使用條件表達式。

條件表達式的基本用法非常簡單,它由三個操作數的運算符構成,形式為:

<表達式 1>?<表達式 2>:<表達式 3>

條件表達式的計算從左往右計算,首先需要計算計算表達式 1 ,其結果類型必須為 Booleanboolean,否則發生編譯錯誤。

當表達式 1 的結果為 true,將會執行表達式 2,否則將會執行表達式 3。

表達式 2 與表達式 3 最後的類型必須得有返回結果,即不能為是 void,若為 void ,編譯時將會報錯。

最後需要注意的是,表達式 2 與表達式 3 不會被同時執行,兩者只有一個會被執行。

踩坑案例

了解完三目運算符的基本原理,我們簡化一下開頭例子,復現一下三目運算符使用過程的一些坑。假設我們的例子簡化成如下:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;

案例 1

第一個案例我們根據如下計算 result 的值。

int result = flag ? nullInteger : simpleInt;

這個案例為開頭的例子的簡化版本,運算上述代碼,將會發生 NPE 的。

為什麼會發發生 NPE 呢?

這裏可以給大家一個小技巧,當我們從代碼上沒辦法找到答案時,我們可以試試查看一下編譯之後字節碼,或許是 Java 編譯之後增加某些東西,從而導致問題。

使用 javap -s -c class 查看 class 文件字節碼,如下:

可以看到字節碼中加入一個拆箱操作,而這個拆箱只有可能發生在 nullInteger

那麼為什麼 Java 編譯器在編譯時會對表達式進行拆箱?難道所有数字類型的包裝類型都會進行拆箱嗎?

條件表達式表達式發生自動拆箱,其實官方在 「The Java Language Specification(簡稱:JLS)」15.25 節中做出一些規定,部分內容如下:

JDK7 規範

If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.

If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

用大白話講,如果表達式 2 與表達式 3 類型相同,那麼這個不用任何轉換,條件表達式表達式結果當然與表達式 2,3 類型一致。

當表達 2 或表達式 3 其中任一一個是基本數據類型,比如 int,而另一個表達式類型為包裝類型,比如 Integer,那麼條件表達式表達式結果類型將會為基本數據類型,即 int

ps:有沒有疑問?為什麼不規定最後結果類型都為包裝類那?

這是 Java 語言層面一種規範,但是這個規範如果強制讓程序員執行,想必平常使用三目運算符將會比較麻煩。所以面對這種情況, Java 在編譯器在編譯過程加入自動拆箱進制。

所以上述代碼可以等同於下述代碼:

int result = flag ? nullInteger.intValue() : simpleInt;

如果我們一開始的代碼如上所示,那麼這裏錯誤點其實就很明顯了。

案例 2

接下來我們在第一個案例基礎上修改一下:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;
Integer objInteger = Integer.valueOf(88);

int result = flag ? nullInteger : objInteger;

運行上述代碼,依然會發生 NPE 的問題。當然這次問題發生點與上一個案例不一樣,但是錯誤原因卻是一樣,還是因為自動拆箱機制導致。

這一次表達式 2 與表達式 3 都為包裝類 Integer,所以條件表達式的最後結果類型也會是 Integer

但是由於 result是 int 基本數據類型,好傢伙,數據類型不一致,編譯器將會對條件表達式的結果進行自動拆箱。由於結果為 null,自動拆箱將報錯了。

上述代碼等同為:

int result = (flag ? nullInteger : objInteger).intValue();

案例 3

我們再稍微改造一下案例 1 的例子,如下所示:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;
Integer result = flag ? nullInteger : simpleInt;

案例 3 與案例 1 右邊部分完全相同,只不過左邊部分的類型不一樣,一個為基本數據類型 int,一個為 Integer

按照案例 1 的分析,這個也會發生 NPE 問題,原因與案例 1 一樣。

這個之所以拿出來,其實想說下,上述條件表達式的結果為 int 類型,而左邊類型為 Integer,所以這裏將會發生自動裝箱操作,將 int類型轉化為 Integer

上述代碼等同為:

Integer result = Integer.valueOf(flag ? nullInteger.intValue() : simpleInt);

案例 4

最後一個案例,與上面案例都不一樣,代碼如下:

boolean flag = true; //設置成true,保證表達式 2 被執行
Integer nullInteger = null;
Long objLong = Long.valueOf(88l);

Object result = flag ? nullInteger : objLong;

運行上述代碼,依然將會發生 NPE 的問題。

這個案例表達式 2 與表達式 3 類型不一樣,一個為 Integer,一個為 Long,但是這兩個類型都是 Number的子類。

面對上述情況,JLS 規定:

Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.

Note that binary numeric promotion performs value set conversion (§5.1.13) and may perform unboxing conversion (§5.1.8).

大白話講,當表達式 2 與表達式 3 類型不一致,但是都為数字類型時,低範圍類型將會自動轉為高範圍數據類型,即向上轉型。這個過程將會發生自動拆箱。

Java 中向上轉型並不需要添加任何轉化,但是向下轉換必須強制添加類型轉換。

上述代碼轉化比較麻煩,我們先從字節碼上來看:

第一步,將 nullInteger拆箱。

第二步,將上一步的值轉為 long 類型,即 (long)nullInteger.intValue()

第三步,由於表達式 2 變成了基本數據類型,表達式 3 為包裝類型,根據案例 1 講到的規則,包裝類型需要轉為基本數據類型,所以表達式 3 發生了拆箱。

第四步,由於條件表達式最後的結果類型為基本數據類型:long,但是左邊類型為 Object,這裏就需要把 long 類型裝箱轉為包裝類型。

所以最後代碼等同於:

Object result = Long.valueOf(flag ? (long)nullInteger.intValue() : objLong.longValue());

總結

看完上述四個案例,想必大家應該會有種感受,沒想到這麼簡單的條件表達式,既然暗藏這麼多「殺機」。

不過大家也不用過度害怕,不使用條件表達式。只要我們在開發過程重點注意包裝類型的自動拆箱問題就好了,另外也要注意條件表達式的計算結果再賦值的時候自動拆箱引發的 NPE 的問題。

最好大家在開發過程中,都遵守一定的規範,即保持表達式 2 與表達式 3 的類型一致,不讓 Java 編譯器有自動拆箱的機會。

建議大家沒事經常看下阿里出品的『Java 開發手冊』,在最新的「泰山版」就增加條件表達式的這一節規範。

ps:公號消息回復:『開發手冊』,獲取最新版的 Java 開發手冊。

最後一定要做好的單元測試,不要慣性思維,覺得這麼簡單的一個東西,看起來根本不可能出錯的。

參考

  1. Java 開發手冊-泰山版
  2. 《Java 開發手冊》解讀:三目運算符為何會導致 NPE?

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

【JVM故事】一個Java字節碼文件的誕生記

萬字長文,完全虛構。

 

 

組裡來了個實習生,李大胖面完之後,覺得水平一般,但還是留了下來,為什麼呢?各自猜去吧。

李大胖也在心裏開導自己,學生嘛,不能要求太高,只要肯上進,慢慢來。就稱呼為小白吧。

小白每天來的很早,走的很晚,都在用功學習,時不時也向別人請教。只是好像天資差了點。

都快一周了,才能寫些“簡單”的代碼,一個註解,一個接口,一個類,都來看看吧:

public @interface Health {

    String name() default "";
}


public interface Fruit {

    String getName();

    void setName(String name);

    int getColor();

    void setColor(int color);
}


@Health(name = "健康水果")
public class Apple implements Fruit {

    private String name;
    private int color;
    private double weight = 0.5;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getColor() {
        return color;
    }

    @Override
    public void setColor(int color) {
        this.color = color;
    }

    public double weight() {
        return weight;
    }

    public void weight(double weight) {
        this.weight = weight;
    }
}

與周圍人比起來,小白進步很慢,也許是自己不夠聰明,也許是自己不適合干這個,小白好像有點動搖了。

這幾天,小白明顯沒有一開始那麼上進了,似乎有點想放棄,這不,趴在桌子上竟然睡着了。

 

(二)

 

在夢中,小白來到一個奇怪又略顯陰森的地方,眼前有一個破舊的小房子,從殘缺不全的門縫裡折射出幾束光線。

小白有些害怕,但還是鎮定了下,深呼吸幾口,徑直朝着小房子走去。

小白推開門,屋裡沒有人。只有一個“機器”在桌子旁大口大口“吃着”東西,背後也不時的“拉出”一些東西。

小白很好奇,就湊了上去,準備仔細打量一番。

“你要幹嘛,別影響我工作”。突然冒出一句話,把小白嚇了一大跳,慌忙後退三步,媽呀,心都快蹦出來了。

“你是誰呀?”,驚慌中小白說了句話。

“我是編譯器”,哦,原來這個機器還會說話,小白這才緩了過來。

“編譯器”,小白好像聽說過,但一時又想不起,於是猜測到。

“網上評論留言里說的小編是不是就是你啊”?

“你才是呢”,編譯器白了一眼,沒好聲氣的說到。

要不是看在長得還行的份上,早就把你趕走了,編譯器心想。

“哦,我想起來了,編譯器嘛,就是編譯代碼的那個東西”,小白恍然大悟到。

“請注意你的言詞,我不是個東西,哦,不對,我是個東西,哦,好像也不對,我。我。”,編譯器自己也快暈了。

編譯器一臉的無奈,遇上這樣的人,今天我認栽了。

小白才不管呢,心想,今天我竟然見到了編譯器,我得好好請教請教他。

那編譯器會幫助她嗎?

 

 

(三)

 

小白再次走上前來,定睛一看,才看清楚,編譯器吃的是Java源碼,拉的是class(字節碼)文件。

咦,為啥這個代碼這麼熟悉呢,不就是我剛剛寫的那些。“停,停,快停下來了”。編譯器被小白叫停了。

“你又要幹嘛啊”?編譯器到。

“嘻嘻,這個代碼是我寫的,我想看看它是怎麼被編譯的”,小白到。

編譯器看了看這個代碼,這麼“簡單”,她絕對是個菜鳥。哎,算了,還是讓她看看吧。

不過編譯器又到,“整個編譯過程是非常複雜的,想要搞清楚裏面的門道是不可能的,今天也就只能看個熱鬧了”。

“編譯后的內容都是二進制數據,再通俗點說,就是一個長長的字節數組(byte[])”,編譯器繼續說,“通常把它寫入文件,就是class文件了”。

“但這不是必須的,也可以通過網絡傳到其它地方,或者保存在內存中,用完之後就丟棄”。

“哇,還可以這樣”,小白有些驚訝。編譯器心想,你是山溝里出來的,沒見過世面,大驚小怪。

繼續到,“從數據結構上講,數組就是一段連續的空間,是‘沒有結構’的,就像一個線段一樣,唯一能做的就是按索引訪問”。

小白到,“編譯后的內容一定很繁多,都放到一個數組裡面,怎麼知道什麼東西都在哪呢?不都亂套了嘛”。

編譯器覺得小白慢慢上道了,心裏有一絲安慰,至少自己的講解不會完全白費。於是繼續到。

“所以JVM的那些大牛們早就設計好了字節碼的格式,而且還把它們放入到了一個字節數組裡面”。

小白很好奇到,“那是怎麼實現的呢”?

“其實也沒有太高深的內容,既然數組是按位置的,那就規定好所有內容的先後順序,一個接一個往數組裡放唄”。

“如果內容的長度是固定(即定長)的,那最簡單,直接放入即可”。

“如果內容長度是不固定(即變長)的,也很簡單,在內容前用一到兩個字節存一下內容的長度不就OK了”。

 

 

(四)

 

“字節碼的前4個字節必須是一個固定的数字,它的十進制是3405691582,大部分人更熟悉的是它的十六進制,0xCAFEBABE”。

“通常稱之為魔術数字(Magic),它主要是用來區分文件類型的”,編譯器到。

“擴展名(俗稱後綴名)不是用來區分文件類型的嗎”?小白說到,“如.java是Java文件,.class是字節碼文件”。

“擴展名確實可以區分,但大部分是給操作系統用的,或給人看到。如我們看到.mp3時知道是音頻、.mp4是知道是視頻、.txt是文本文件”。

“操作系統可以用擴展名來關聯打開它的軟件,比如.docx就會用word來打開,而不會用文本文件”。編譯器繼續到。

“還有一個問題就是擴展名可以很容易被修改,比如把一個.java手動改為.class,此時讓JVM來加載這個假的class文件會怎樣呢”?

“那JVM先讀取開頭4個字節,發現它不是剛剛提到的那個魔數,說明它不是合法的class文件,就直接拋異常唄”,小白說到。

“很好,真是孺子可教”,編譯器說道,“不過還有一個問題,不知你是否注意到?4個字節對應Java的int類型,int類型的最大值是2147483647”。

“但是魔數的值已經超過了int的最大值,那怎麼放得下呢,難道不會溢出嗎”?

“確實啊,我怎麼沒發現呢,那它到底是怎麼放的呢”?小白到。

“其實說穿了不值得一提,JVM是把它當作無符號數對待的。而Java是作為有符號數對待的。無符號數的最大值基本上是有符號數最大值的兩倍”。

“接下來的4個字節是版本號,不同版本的字節碼格式可能會略有差異,其次在運行時會校驗,如JDK8編譯后的字節碼是不能放到JDK7上運行的”。

“這4個字節中的前2個是次(minor)版本,后2個是主(major)版本”。編譯器繼續到,“比如我現在用的JDK版本是1.8.0_211,那次版本就是0,主版本就是52”。

“所以前8個字節的內容是,0xCAFEBABE,0,52,它們並不是源代碼里的內容”。

Magic [getMagic()=0xcafebabe]
MinorVersion [getVersion()=0]
MajorVersion [getVersion()=52]

 

(五)

 

當編譯器讀到源碼中的public class的時候,然後就就去查看一個表格,如下圖:

自顧自的說著,“public對應的是ACC_PUBLIC,值為0x0001,class默認就是,然後又讀ACC_SUPER的值0x0020”。

“最後把它倆合起來(按位或操作),0x0001 | 0x0020 => 0x0021,然後把這個值存起來,這就是這個類的訪問控制標誌”。

小白這次算是開了眼界了,只是還有一事不明,“這個ACC_SUPER是個什麼鬼”?

編譯器解釋到,“這是歷史遺留問題,它原本表達在調用父類方法時會特殊處理,不過現在已經不再管它了,直接忽略”。

接着讀到了Apple,它是類名。編譯器首先要獲取類的全名,org.cnt.java.Apple。

然後對它稍微轉換一下形式,變成了,org/cnt/java/Apple,“這就是類名在字節碼中的表示”。

編譯器發現這個Apple類沒有顯式繼承父類,表明它繼承自Object類,於是也獲取它的全名,java/lang/Object。

接着讀到了implements Fruit,說明該類實現了Fruit接口,也獲取全名,org/cnt/java/Fruit。

小白說到,“這些比較容易理解,全名中把點號(.)替換為正斜線(/)肯定也是歷史原因了。但是這些信息如何存到數組裡呢”?

“把點號替換為正斜線確實是歷史原因”,編譯器繼續到,“這些字符串雖然都是類名或接口名,但本質還是字符串,類名、接口名只是賦予它的意義而已”。

“除此之外,像字段名、方法名也都是字符串,同理,字段名、方法名也是賦予它的意義。所以字符串是一種基本的數據,需要得到支持”。

“除了字符串之外,還有整型数字,浮點数字,這些也是基本的數據,也需要得到支持”。

因此,設計者們就設計出了以下幾種類型,如圖:

“左邊是類型名稱,方便理解,右邊是對應的值,用於存儲”,編譯器繼續到。

“這裏的Integer/Long/Float/Double和Utf8都是具體保存數據用的,表示整型數/浮點數和字符串。其它的類型大都是對字符串的引用,並賦予它一定的意義”。

“所以類名首先被存儲為一個字符串,也就是Utf8,它的值對應的是1”。編譯器接着到,“由於字符串是一個變長的,所以就先用兩個字節存儲字符串的長度,接着跟上具體的字符串內容”。

所以字符串的結構就是這樣,如圖:

“類名字符串的存儲數據為,1、18、org/cnt/java/Apple。第一個字節為1,表明是Utf8類型,第2、3兩個字節存儲18,表示字符串長度是18,接着存儲真正的字符串。所以共用去1 + 2 + 18 => 21個字節”。

“父類名字符串存儲為,1、16、java/lang/Object。共用去19個字節”。

“接口名字符串存儲為,1、18、org/cnt/java/Fruit。共用去21個字節”。

小白聽的不住點頭,編譯器喘口氣,繼續講解。

“字符串存好后,就該賦予它們意義了,在後續的操作中肯定涉及到對這些字符串的引用,所以還要給每個字符串分配一個編號”。

如Apple為#2,即2號,Object為#4,Fruit為#6。

“由於這三個字符串都是類名或接口名,按照設計規定應該使用Class表示,對應的值為7,然後再指定一個字符串的編號即可”。

因此類或接口的表示如下圖:

“先用1個字節指明是類(接口),然後再用2個字節存儲一個字符串的編號。整體意思很直白,就是把這個編號的字符串當作類名或接口名”。

“類就表示為,7、#2。7表示是Class,#2表示類名稱那個字符串的存儲編號。共用去3個字節”。

“父類就表示,7、#4。共用去3個字節。接口就表示為,7、#6。共用去3個字節”。

其實這三個Class也分別給它們一個編號,方便別的地方再引用它們。

 

 

(六)

 

“其實上面這些內容都是常量,它們都位於常量池中,它們的編號就是自己在常量池中的索引”。編譯器說到。

“常量池很多人都知道,起碼至少是聽說過。但絕大多數人對它並不十分熟悉,因為很少有人見過它”。

編譯器繼續到,“今天你可算是來着了”,說著就把小白寫的類編譯後生成的常量池擺到了桌子上。

“這是什麼東西啊,這麼多,又很奇怪”,小白說到,這也是她第一次見。

ConstantPoolCount [getCount()=46]
ConstantPool [
#0 = null
#1 = ConstantClass [getNameIndex()=2, getTag()=7]
#2 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Apple, getTag()=1]
#3 = ConstantClass [getNameIndex()=4, getTag()=7]
#4 = ConstantUtf8 [getLength()=16, getString()=java/lang/Object, getTag()=1]
#5 = ConstantClass [getNameIndex()=6, getTag()=7]
#6 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Fruit, getTag()=1]
#7 = ConstantUtf8 [getLength()=4, getString()=name, getTag()=1]
#8 = ConstantUtf8 [getLength()=18, getString()=Ljava/lang/String;, getTag()=1]
#9 = ConstantUtf8 [getLength()=5, getString()=color, getTag()=1]
#10 = ConstantUtf8 [getLength()=1, getString()=I, getTag()=1]
#11 = ConstantUtf8 [getLength()=6, getString()=weight, getTag()=1]
#12 = ConstantUtf8 [getLength()=1, getString()=D, getTag()=1]
#13 = ConstantUtf8 [getLength()=6, getString()=<init>, getTag()=1]
#14 = ConstantUtf8 [getLength()=3, getString()=()V, getTag()=1]
#15 = ConstantUtf8 [getLength()=4, getString()=Code, getTag()=1]
#16 = ConstantMethodRef [getClassIndex()=3, getNameAndTypeIndex()=17, getTag()=10]
#17 = ConstantNameAndType [getNameIndex()=13, getDescriptorIndex()=14, getTag()=12]
#18 = ConstantDouble [getDouble()=0.5, getTag()=6]
#19 = null
#20 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=21, getTag()=9]
#21 = ConstantNameAndType [getNameIndex()=11, getDescriptorIndex()=12, getTag()=12]
#22 = ConstantUtf8 [getLength()=15, getString()=LineNumberTable, getTag()=1]
#23 = ConstantUtf8 [getLength()=18, getString()=LocalVariableTable, getTag()=1]
#24 = ConstantUtf8 [getLength()=4, getString()=this, getTag()=1]
#25 = ConstantUtf8 [getLength()=20, getString()=Lorg/cnt/java/Apple;, getTag()=1]
#26 = ConstantUtf8 [getLength()=7, getString()=getName, getTag()=1]
#27 = ConstantUtf8 [getLength()=20, getString()=()Ljava/lang/String;, getTag()=1]
#28 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=29, getTag()=9]
#29 = ConstantNameAndType [getNameIndex()=7, getDescriptorIndex()=8, getTag()=12]
#30 = ConstantUtf8 [getLength()=7, getString()=setName, getTag()=1]
#31 = ConstantUtf8 [getLength()=21, getString()=(Ljava/lang/String;)V, getTag()=1]
#32 = ConstantUtf8 [getLength()=16, getString()=MethodParameters, getTag()=1]
#33 = ConstantUtf8 [getLength()=8, getString()=getColor, getTag()=1]
#34 = ConstantUtf8 [getLength()=3, getString()=()I, getTag()=1]
#35 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=36, getTag()=9]
#36 = ConstantNameAndType [getNameIndex()=9, getDescriptorIndex()=10, getTag()=12]
#37 = ConstantUtf8 [getLength()=8, getString()=setColor, getTag()=1]
#38 = ConstantUtf8 [getLength()=4, getString()=(I)V, getTag()=1]
#39 = ConstantUtf8 [getLength()=3, getString()=()D, getTag()=1]
#40 = ConstantUtf8 [getLength()=4, getString()=(D)V, getTag()=1]
#41 = ConstantUtf8 [getLength()=10, getString()=SourceFile, getTag()=1]
#42 = ConstantUtf8 [getLength()=10, getString()=Apple.java, getTag()=1]
#43 = ConstantUtf8 [getLength()=25, getString()=RuntimeVisibleAnnotations, getTag()=1]
#44 = ConstantUtf8 [getLength()=21, getString()=Lorg/cnt/java/Health;, getTag()=1]
#45 = ConstantUtf8 [getLength()=12, getString()=健康水果, getTag()=1]
]

“在常量池前面會用2個字節來存儲常量池的大小,需要記住的是,這個大小不一定就是池中常量的個數。但它減去1一定是最大的索引”。

“因為,常量池中為0的位置(#0)永遠不使用,還有Long和Double類型一個常量佔2個連續索引(沒錯,又是歷史原因),實際只是用了第1個索引,第2個索引永遠空着(參見#18、#19)”。

編譯器繼續到,“#0是特殊的,用來表示‘沒有’的意思,其它地方如果想表達沒有的話,可以指向它。如Object是沒有父類的,所以它的父類指向#0,即沒有”。

“所以常量都是從#1開始。可以看看#1到#6的內容,就是剛剛上面講的”。編譯器說到。

“真是學到不少知識啊”,小白說到,“關於常量池能不能再多講點”?編譯器只好繼續講。

 

 

(七)

 

“常量池就是一個容器,它裏面放了各種各樣的所有信息,並且為每個信息分配一個編號(即索引),如果想要在其它地方使用這些信息,直接使用這個編號就行了”。

編譯器繼續到,“這個常量池在一些語言中也被稱為‘符號表’,通過編號來使用的這種方式也被稱為‘符號引用’”。

相信很多愛學習的同學對符號表和符號引用這兩個詞都很熟悉,不管之前是不是真懂,至少現在應該是真的搞懂了。因為你已經看到了。

“採用這種常量池和常量引用方式的好處其實很多,就說個最容易想到的,就是重複利用,節省空間,便於管理”。編譯器繼續說。

“比如一個類里有10個方法,每個方法里都定義一個length的局部變量,那麼length這個名字就會出現在常量池裡面,且只會出現一次,那10個方法都是對它的引用而已”。

“如果有一個方法的名字也叫length的話,那也是對同一個常量的引用,因為這個length常量只是個字符串數據而已,本身沒有明確含義,它的含義來自於引用它的常量”。

“哦,原來如此”,小白開悟到,“‘符號表、符號引用’這些‘高大上’的叫法,不過就是根據索引去列表裡獲取元素罷了”,哈哈。

編譯器看到小白這麼開心,就準備拋出一個問題,“打壓”一下她。於是說到。

“常量池看上去和數組/列表非常相似,都是容器且都是基於索引訪問的。為啥常量池只被稱為符號表,而不是符號數組或符號列表呢”?

小白自然回答不上來。編譯器繼續說,“表的英文單詞是Table。它和數組/列表的唯一區別就是,數組/列表裡的元素長度都是固定的。表裡的元素長度是不固定的”。

“常量池中的好幾種常量的長度都是變長的,所以自然是表了”。

小白點了點頭,心裏想,這編譯器就是厲害,我這輩子看來都無法達到他的高度了。

編譯器繼續說到,“字節碼的前8個字節存儲魔數和版本,接着的2個(9和10)字節存儲常量池的大小,後面接着(從11開始)就是整個常量池的內容了”。

“之所以把常量池放這麼靠前,是因為後面的所有內容都要依賴它、引用它”。

緊跟在常量池之後的就是這個類的基本信息,如下:

“首先用2個字節存儲上面已經計算好的訪問控制標誌,即0x0021”。

“然後用2個字節存儲這個類在常量池中的索引,就是#1”。

“然後用2個字節存儲該類的父類在常量池中的索引,就是#3”。

“由於接口可以有多個,所以再用2個字節存儲接口的個數,因為只實現了1個接口,所以就存儲数字1”。

“接着存儲所有接口在常量池中的索引,每個接口用2個字節。因為只實現了1個接口,所以存儲的索引就是#5”。

AccessFlags [getAccessFlags()=0x21, getAccessFlagsString()=[ACC_PUBLIC, ACC_SUPER]]
ThisClass [getClassIndex()=1, getClassName()=org/cnt/java/Apple]
SuperClass [getClassIndex()=3, getClassName()=java/lang/Object]
InterfacesCount [getCount()=1]
Interfaces [getClassIndexes()=[5], getClassNames()=[org/cnt/java/Fruit]]

 

 

 

(八)

 

編譯器繼續到,“接下來該讀取字段信息了”。當讀到private時,就去下面這張表裡找:

找到ACC_PRIVATE,把它的值0x0002保存以下,這就是該字段的訪問控制標誌。

接着讀到的是String,這是字段的類型,然後會把這個String類型存入常量池,對應的索引是#8。

可以看到是一個Utf8,說明是字符串,內容是 Ljava/lang/String; ,以大寫L開頭,已分號;結尾,中間是類型全名,這是在字節碼中表示類(對象)類型的方式。

接着讀到的是name,這是字段名稱,也是個字符串,同樣也把它放入常量池,對應的索引是#7。

編譯器說到,“現在一個字段的信息已經讀取完畢,按照相同的方式把剩餘的兩個字段也讀取完畢”。

“那字段的信息又該怎麼存儲呢”?小白問到。“不要着急嘛”,編譯器說著就拿出了字段的存儲格式:

首先2個字節是訪問控制標誌,接着2個字節是字段名稱在常量池中的索引,接着2個字節是字段描述(即類型)在常量池中的索引。

接着2個字節就是屬性個數,然後就是具體的屬性信息了。例如字段上標有註解的話,這個註解信息就會放入屬性信息里。

編譯器繼續說到,“屬性信息是字節碼中比較複雜的內容,這裏就不說太多了”。接着就可以按格式整理數據了。

因為一個類的字段可以有多個,所以先用2個字節存儲一下字段數目,本類有3個字段,所以就存儲個3。

第一個字段,0x0002、#7、#8、0。共用去8個字節,因為自動沒有屬性內容。

第二個字段,0x0002、#9、#10、0。共用去8個字節。

第二個字段,0x0002、#11、#12、0。共用去8個字節。

編譯器接着說,“所以存儲這3個字段信息共用去2 + 8 + 8 + 8 => 26個字節”。

小白說到,“我現在基本已經搞明白套路了。其實有些東西沒有想象中的那麼複雜啊”。

“複雜的東西還是有的,我們現在先不考慮”,編譯器說到,“還有一個問題,不知你發現了沒有”。

字段color的類型是int,但是在常量池中卻變為大寫字母I,同樣weight的類型是double,常量池中卻是大寫字母D。

小白說到,“我來猜測一下吧,int、double是Java中的數據類型,I、D是與之對應的在JVM中的表示形式。對吧”?

“算你聰明”,編譯器說到,“其實Java和JVM之間關於類型這塊有一個映射表”,如下:

有兩個需要注意。“第一點上面已經說過了,就是類都會映射成LClassName;這種形式,如Object映射為Ljava/lang/Object;”。

第二點是數組,“數組在Java中用一對中括號([])表示,在JVM中只用左中括號([)表示。也就是[]映射為[”。

“多維數組也一樣,[][][]映射為[[[”。然後還有類型,“Java是把類型放到前面,JVM是把類型放到後面”。如double[]映射為[D。

“double[][][]映射為[[[D”。同理,“String[]映射為[Ljava/lang/String;,Object[][]映射為[[Ljava/lang/Object;”。

“我似乎又明白了一些,Java有自己的規範,字節碼也有自己的規範,它們之間的映射關係早都已經定義好了”。小白繼續到。

“只要按照這種映射關係,就能把Java源碼給轉換為字節碼。是吧”?

“粗略來說,可以這麼理解,其實這就是編譯了,但一定要清楚,真正的編譯是非常複雜的一個事情”,編譯器到。

小白說到,“字段完了之後,肯定該方法了,就交給我吧,讓我也試試”。

“年輕人啊,就是生猛,你來試試吧”。編譯器說到。

FieldsCount [getCount()=3]
Fields [
#0 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=7, getName()=name, getDescriptorIndex()=8, getDescriptor()=Ljava/lang/String;, getAttributesCount()=0, getAttributes()=[]]
#1 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=9, getName()=color, getDescriptorIndex()=10, getDescriptor()=I, getAttributesCount()=0, getAttributes()=[]]
#2 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=12, getDescriptor()=D, getAttributesCount()=0, getAttributes()=[]]
]

 

 

 

(九)

 

小白說,“方法呢肯定也有自己的格式,你把它找出來我看看”。

“好好,我這就找”,編譯器苦笑到。我堂堂一個編譯器,今天竟然成了小白的助手,慚愧啊。

說著編譯器就找到了,於是放到了桌子上:

“咦,怎麼和字段的一模一樣”,小白到。那這就更簡單了。

先是訪問控制標誌,接着是方法名稱索引,然後是方法描述索引,最後是和方法關聯的屬性。於是照貓畫虎,小白就開始了。

先讀到public關鍵字,這是個訪問控制修飾符,肯定也有一張表和它對應,可以找到這個關鍵字對應的數值。

還沒等小白開口,編譯器就趕緊把表找出來了:

小白繼續,ACC_PUBLIC對應的值是0x0001,就把這個值先保存起來。

然後是方法的名字,getName,是一個字符串,照例把它存入常量池,並且有一個索引,就是#26。

接着該方法的描述了,小白認為方法和字段是不同的,除了有返回類型之外,還有參數呢,這該咋整呢?

於是就問編譯器,“方法的描述應該也有格式吧”?

“你越來越聰明了”,編譯器說到,“其實也很簡單,我來簡單說下吧”。

“在Java中如果把訪問控制符、方法名、參數名、方法體都去掉,其實就剩下‘方法簽名’了”。

例如,沒有入參沒有返回值的,就是這個樣子,void()。

返回值為String,入參為int,double,String的,其實就是這樣個子,String(int, double, String)。

“這個方法簽名其實就是在Java中對方法的描述,在字節碼中和它差不多,就是把返回類型放到後面,把參數間的逗號去掉”。

因此void()映射為()V,這裏要注意的是void對應的是大寫字母V。

String(int, double, String)映射為(IDLjava/lang/String;)Ljava/lang/String;

“不難,不難”,小白說到,於是又繼續開始了。

小白按照這種格式,把剛剛的那個方法描述也存入了常量池,得到的索引就是#27。

小白按這個套路把6個方法都整理好了,接下來該按格式把數據寫入字節數組了。

編程新說注:方法的代碼對應的是JVM的指令,這裏就忽略不談了,後續可能會單獨再說。

編譯器提醒小白說,“你是不是還漏掉了一個方法啊”?

小白又看了一遍Java源碼,仔細數了數,是6個呀,沒錯啊。

編譯器說到,你在學習時有沒有見過這樣一句話,“當類沒有定義構造函數時,編譯器會為它生成一個默認的無參構造函數”。

小白連忙點頭,“嗯嗯嗯,見過的”。

“這就是了”,編譯器說道,“不過需要注意的是,在字節碼中構造方法的名字都是<init>,返回類型都是V”。

“這也是規定的吧”,小白說到,編譯器點了點頭。

編譯器又說到,“其實還有方法的參數信息,如參數位置,參數類型,參數名稱,參數的訪問控制標誌等”。

“這些信息都是放在方法格式里最後的屬性信息中的,咱們也暫時不說它們了”。

編程新說注

在JDK7及以前,字節碼中不包含方法的參數名。因為JVM執行指令時,參數是按位置傳入的,所以參數名對代碼的執行沒有用處。

由於越來越多的框架採用按方法參數名進行數值綁定,Java也只好在JDK8時加入了對參數名的支持。

不過需要設置一下編譯器的–parameters參數,這樣才能把方法參數名也放入字節碼中。

可以看看常量池中的#32是“MethodParameters”字符串,說明字節碼中已經包含參數名了。

常量池中#7、#9、#11三個字符串就是參數名,同時也是字段名,這就是復用的好處。

編程新說注方法的格式和字段的格式完全一樣,就不再演示寫入過程了。

因此這個類共有7個方法。

MethodsCount [getCount()=7]
Methods [
#0 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=13, getName()=<init>, getDescriptorIndex()=14, getDescriptor()=()V, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=1, getCodeLength()=12, getJvmCode()=JvmCode [getCode()=12], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=3, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=8], LineNumTable [getStartPc()=4, getLineNumber()=12], LineNumTable [getStartPc()=11, getLineNumber()=8]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=12, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#1 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=26, getName()=getName, getDescriptorIndex()=27, getDescriptor()=()Ljava/lang/String;, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=16]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#2 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=30, getName()=setName, getDescriptorIndex()=31, getDescriptor()=(Ljava/lang/String;)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=21], LineNumTable [getStartPc()=5, getLineNumber()=22]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=7, getDescriptorIndex()=8, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=7, getAccessFlags()=0x0]]]]]
#3 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=33, getName()=getColor, getDescriptorIndex()=34, getDescriptor()=()I, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=26]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#4 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=37, getName()=setColor, getDescriptorIndex()=38, getDescriptor()=(I)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=31], LineNumTable [getStartPc()=5, getLineNumber()=32]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=9, getDescriptorIndex()=10, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=9, getAccessFlags()=0x0]]]]]
#5 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=39, getDescriptor()=()D, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=35]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#6 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=40, getDescriptor()=(D)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=3, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=39], LineNumTable [getStartPc()=5, getLineNumber()=40]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=11, getDescriptorIndex()=12, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=11, getAccessFlags()=0x0]]]]]
]

編程新說注方法部分的輸出內容很多,是因為包含了方法體的代碼的信息。

 

 

(十)

 

“真是後生可畏啊”,編譯器感慨到。“小白竟然也能按照套路去在做點事情了”。

不過編譯器並不自危,因為最核心的內容是,可執行代碼如何轉換為JVM指令集中的指令,這可是“壓箱底”的乾貨,可不能隨便告訴別人,長得再好看也不行。哈哈,O(∩_∩)O。

接着編譯器拿出一個完整的字節碼文件格式圖給小白看:

小白看完后說,“和剛剛講的一樣,只是最後也有這個屬性信息啊”。

編譯器說,“屬性信息是字節碼文件中非常複雜的內容,可以暫時不管用了”。

上面已經說了,至少註解的相關內容是放在屬性信息里的。

那就看看你寫的這個類的屬性信息都是什麼吧:

AttributesCount [getCount()=2]
Attributes [
#0 = SourceFile [getSourcefileIndex()=42]
#1 = RuntimeVisibleAnnotations [getNumAnnotations()=1, getAnnotations()=[Annotation [getTypeIndex()=44, getNumElementValuePairs()=1, getElementValuePairs()=[ElementValuePair [getElementNameIndex()=7, getElementValue()=ElementValue [getTag()=ElementValueTag [getTagChar()=s], getUnion()=ElementValueUnion [getConstValueIndex()=45]]]]]]]
]

編譯器繼續說,共有2條屬性信息,第一條是源代碼文件的名字,在常量池中的#42。其實就是Apple.java了。

第二條是運行時可見的註解信息,本類共有1個註解,註解類型是常量池中的#44。其實就是Lorg/cnt/java/Health;了。

該註解共顯式設置了1對屬性值。屬性名稱是常量池中的#7,就是name了,類型是小寫的s,表示String類型,屬性值是#45,也就是“健康水果”了。

下圖中的這些類型,都是可以用於註解屬性的類型:

最後,編譯器打印出一行信息:

—–bytes=1085—–

小白說,“這是什麼意思”?“這是編譯后產生的字節碼的總長度,是1085個字節”,編譯器到。

小白剛想表達對編譯器的感謝,忽然聞到一陣香味,而且是肉香。

PS:最後幾句話就不寫了,請你來補充完整吧,嘻嘻。

 

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)

【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

 

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

 

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

【其他文章推薦】

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

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

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

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

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

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

※回頭車貨運收費標準

C#中的Singleton模式

目錄

  • 前言
  • 實現思路
  • 實現方法
    • 最簡單的實現方法
    • 如果多線程亂入?
    • 線程安全的單例模式
      • Lock版本
      • 靜態構造器版本
      • Lazy版本
  • 總結

前言

Singleton是二十三個設計模式中比較重要也比較經常使用的模式。但是這個模式雖然簡單,實現起來也會有一些小坑,讓我們一起來看看吧!

實現思路

首先我們看看這個設計模式的UML類圖。

很清晰的可以看到,有三點是需要我們在實現這個模式的時候注意的地方。

  • 私有化的構造器
  • 全局唯一的靜態實例
  • 能夠返回全局唯一靜態實例的靜態方法

其中,私有化構造器是防止外部用戶創建新的實例而靜態方法用於返回全局唯一的靜態實例供用戶使用。原理清楚了,接下來我們看看一些典型的實現方式和其中的暗坑。

實現方法

最簡單的實現方法

最簡單的實現方法自然就是按照UML類圖直接寫一個類,我們看看代碼。

    class Program
    {
        static void Main(string[] args)
        {
        	var single1 = Singleton.Instance;
            var single2 = Singleton.Instance;
            Console.WriteLine(object.ReferenceEquals(single1, single2));
            Console.ReadLine();
        }
    }

    class Singleton
    {
        private static Singleton _Instance = null;
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        public static Singleton Instance
        {
            get
            {
                if (_Instance == null)
                {
                    _Instance = new Singleton();
                }
                return _Instance;
            }
        }

        public void DumbMethod()
        {

        }
    }

這段代碼忠實的實現了UML類圖裡面的一切,查看輸出結果,

證實了Singleton確實起了作用,多次調用僅僅產生了一個實例,似乎這麼寫就可以實現這個模式了。但是,真的會那麼簡單嗎?

如果多線程亂入?

現在我們給剛剛的例子加點調料,假設多個對實例的調用,並不是簡單的,彬彬有禮的順序關係,二是以多線程的方式調用,那麼剛剛那種實現方法,還能從容應對嗎?讓我們試試。把Main函數裏面的調用改成這樣。

	static void Main(string[] args)
        {
            int TOTAL = 10000;
            Task[] tasks = new Task[TOTAL];
            for (int i = 0; i < TOTAL; i++)
            {
                tasks[i] = Task.Factory.StartNew(() =>
                {
                    Singleton.Instance.DumbMethod();
                });
            }
			Task.WaitAll(tasks);
            Console.ReadLine();
        }

通過Factory創造出1萬個Task,幾乎同時去請求這個單例,看看輸出。

咦,我們剛剛寫的Singleton模式失效了,這個類被創造了5次(這段代碼運行多次,這個数字不一定相同),一定是多線程搞的鬼,我們剛剛寫的代碼沒有辦法應對多線程,換句話說,是非線程安全的(thread-safe),那有沒有辦法來攻克這個難關呢?

線程安全的單例模式

Lock版本

提到線程安全,很多同學第一反應就是用lock,不錯,lock是個可行的辦法,讓我們試試。添加一個引用類型的對象作為lock對象,修改代碼如下(什麼?你問我為什必須是引用類型的對象而不能是值類型的對象?因為lock的時候,如果對象是值類型,那麼lock僅僅鎖住了它的一個副本,另外一個線程可以暢通無阻的再次lock,這樣lock就失去了阻塞線程的意義)

	private static object _SyncObj = new object();
        public static Singleton Instance
        {
            get
            {
                lock (_SyncObj)
                {
                    if (_Instance == null)
                    {
                        _Instance = new Singleton();
                    }
                    return _Instance;
                }                
            }
        }

運行一下,輸出

只有一個實例創建,證明Lock起作用了,這個模式可行!不過有些不喜歡用Lock的同學可能要問,還有沒有其他辦法呢?答案是有的。

靜態構造器版本

回想一下,C#中的類靜態構造器,只會在這個類第一次被使用的時候調用一次,天然的線程安全,那我們試試不用Lock使用類靜態構造器?修改Singleton類如下:

    class Singleton
    {
        private static Singleton _Instance = null;
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        static Singleton()
        {
            _Instance = new Singleton();
        }

        //private static object _SyncObj = new object();
        public static Singleton Instance
        {
            get { return _Instance; }
        }

        public void DumbMethod()
        {

        }
    }

去掉了Lock,添加了一個類靜態構造器,試一試。

完美!對於不喜歡用Lock(在這個例子中,實例只會創建一次但是之後的所有線程都要先排隊Lock再進入Critical code進行檢查,效率比較低下)的同學,類靜態構造器提供了一種很好的選擇。
不過俗話說,人心苦不足 , 我們總是追求卓越。這個版本比Lock版本似乎更好一點,那還有沒有更好的版本呢?有的。

Lazy版本

從net 4.0開始,C#開始支持延遲初始化,通過Lazy關鍵字,我們可以聲明某個對象為僅僅當第一次使用的時候,再初始化,如果一直沒有調用,那就不初始化,省去了一部分不必要的開銷,提升了效率。如果你不熟悉Lazy或者想更多了解它,請參考。我們今天關注的重點在於,Lazy也是天生線程安全的,所以我們嘗試用它來實現Singleton模式?修改代碼如下:

    class Singleton
    {
        private static Lazy<Singleton> _Instance = new Lazy<Singleton>(() => new Singleton());
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        public static Singleton Instance
        {
            get
            {
                return _Instance.Value;
            }
        }

        public void DumbMethod()
        {

        }
    }

輸出結果中可以看到,我們達到了想要的效果:

在上面的代碼中,私有變量_Instance現在是被聲明為延遲初始化,這樣不但天然實現了線程安全,同時在沒有調用Instance靜態方法的時候(也即沒有調用_Instance.Value),初始化不會發生,這樣就提高了效率。

總結

Singleton模式很常見,實現起來也很簡單,只是要小心線程安全。以上三種方法都可以實現線程安全的Singleton模式。如果net 版本在4.0之上,建議使用Lazy版本,畢竟對比Lock版本,Lazy版本可以免去實現手動Lock之苦,對比Static版本,又有延遲初始化的性能優勢,何樂而不為呢?

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)

系列文章

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

上篇文章完成了分類和標籤頁面相關的共6個接口,本篇繼續來寫博客增刪改查API的業務。

供前端查詢用的接口還剩下一個,這裏先補上。

友鏈列表

分析:返回標題和對應的鏈接即可,傳輸對象FriendLinkDto.cs

//FriendLinkDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class FriendLinkDto
    {
        /// <summary>
        /// 標題
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 鏈接
        /// </summary>
        public string LinkUrl { get; set; }
    }
}

添加查詢友鏈列表接口和緩存接口。

//IBlogService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync();
    }
}
//IBlogCacheService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync(Func<Task<ServiceResult<IEnumerable<FriendLinkDto>>>> factory);
    }
}

接下來,實現他們。

//BlogCacheService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.Application.Caching.Blog.Impl
{
    public partial class BlogCacheService
    {
        private const string KEY_QueryFriendLinks = "Blog:FriendLink:QueryFriendLinks";

        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync(Func<Task<ServiceResult<IEnumerable<FriendLinkDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryFriendLinks, factory, CacheStrategy.ONE_DAY);
        }
    }
}
//BlogService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.Domain.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync()
        {
            return await _blogCacheService.QueryFriendLinksAsync(async () =>
            {
                var result = new ServiceResult<IEnumerable<FriendLinkDto>>();

                var friendLinks = await _friendLinksRepository.GetListAsync();
                var list = ObjectMapper.Map<IEnumerable<FriendLink>, IEnumerable<FriendLinkDto>>(friendLinks);

                result.IsSuccess(list);
                return result;
            });
        }
    }
}

直接查詢所有的友鏈數據,這裏使用前面講到的AutoMapper處理對象映射,將IEnumerable<FriendLink>轉換為IEnumerable<FriendLinkDto>

MeowvBlogAutoMapperProfile.cs中添加一條配置:CreateMap<FriendLink, FriendLinkDto>();,在BlogController中添加API。

/// <summary>
/// 查詢友鏈列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("friendlinks")]
public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync()
{
    return await _blogService.QueryFriendLinksAsync();
}

編譯運行,打開查詢友鏈的API,此時沒數據,手動添加幾條數據進去再試試吧。

文章管理

後台文章管理包含:文章列表、新增、更新、刪除文章,接下來依次完成這些接口。

文章列表

這裏的文章列表和前台的文章列表差不多,就是多了一個Id,以供編輯和刪除使用,所以可以新建一個模型類QueryPostForAdminDto繼承QueryPostDto,添加PostBriefForAdminDto繼承PostBriefDto同時新增一個字段主鍵Id。

QueryPostForAdminDto中隱藏基類成員Posts,使用新的接收類型:IEnumerable<PostBriefForAdminDto>

//PostBriefForAdminDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}
//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class QueryPostForAdminDto : QueryPostDto
    {
        /// <summary>
        /// Posts
        /// </summary>
        public new IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

添加分頁查詢文章列表的接口:QueryPostsForAdminAsync(),關於後台的一些接口就不添加緩存了。

//IBlogService.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync(PagingInput input);
    }
}

然後實現這個接口。

//BlogService.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using Meowv.Blog.ToolKits.Extensions;
using System.Linq;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync(PagingInput input)
        {
            var result = new ServiceResult<PagedList<QueryPostForAdminDto>>();

            var count = await _postRepository.GetCountAsync();

            var list = _postRepository.OrderByDescending(x => x.CreationTime)
                                      .PageByIndex(input.Page, input.Limit)
                                      .Select(x => new PostBriefForAdminDto
                                      {
                                          Id = x.Id,
                                          Title = x.Title,
                                          Url = x.Url,
                                          Year = x.CreationTime.Year,
                                          CreationTime = x.CreationTime.TryToDateTime()
                                      })
                                      .GroupBy(x => x.Year)
                                      .Select(x => new QueryPostForAdminDto
                                      {
                                          Year = x.Key,
                                          Posts = x.ToList()
                                      }).ToList();

            result.IsSuccess(new PagedList<QueryPostForAdminDto>(count.TryToInt(), list));
            return result;
        }
    }
}

實現邏輯也非常簡單和之前一樣,就是在Select的時候多了一個Id,添加一個新的Controller:BlogController.Admin.cs,添加這個接口。

//BlogController.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.HttpApi.Controllers
{
    public partial class BlogController
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet]
        [Authorize]
        [Route("admin/posts")]
        [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
        public async Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync([FromQuery] PagingInput input)
        {
            return await _blogService.QueryPostsForAdminAsync(input);
        }
    }
}

因為是後台的接口,所以加上AuthorizeAttribute,指定接口組為GroupName_v2,參數方式為[FromQuery]

當沒有進行授權的時候,是無法訪問接口的。

新增文章

在做新增文章的時候要注意幾點,不是單純的添加文章數據就結束了,要指定文章分類,添加文章的標籤。添加標籤我這裡是從標籤庫中去取得數據,只存標籤Id,所以添加標籤的時候就可能存在添加了標籤庫中已有的標籤。

新建一個新增和更新文章的通用輸出參數模型類,起名:EditPostInput,繼承PostDto,然後添加標籤Tags字段,返回類型IEnumerable<string>

//EditPostInput.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog.Params
{
    public class EditPostInput : PostDto
    {
        /// <summary>
        /// 標籤列表
        /// </summary>
        public IEnumerable<string> Tags { get; set; }
    }
}

添加新增文章的接口:InsertPostAsync

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<ServiceResult> InsertPostAsync(EditPostInput input);

然後去實現這個接口,實現之前,配置AutoMapper實體映射。

CreateMap<EditPostInput, Post>().ForMember(x => x.Id, opt => opt.Ignore());

EditPostInput轉換為Post,並且忽略Id字段。

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ServiceResult> InsertPostAsync(EditPostInput input)
{
    var result = new ServiceResult();

    var post = ObjectMapper.Map<EditPostInput, Post>(input);
    post.Url = $"{post.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{post.Url}/";
    await _postRepository.InsertAsync(post);

    var tags = await _tagRepository.GetListAsync();

    var newTags = input.Tags
                       .Where(item => !tags.Any(x => x.TagName.Equals(item)))
                       .Select(item => new Tag
                       {
                           TagName = item,
                           DisplayName = item
                       });
    await _tagRepository.BulkInsertAsync(newTags);

    var postTags = input.Tags.Select(item => new PostTag
    {
        PostId = post.Id,
        TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id
    });
    await _postTagRepository.BulkInsertAsync(postTags);

    result.IsSuccess(ResponseText.INSERT_SUCCESS);
    return result;
}

URL字段,根據創建時間按照yyyy/MM/dd/name/格式拼接。

然後找出是否有新標籤,有的話批量添加至標籤表。

再根據input.Tags構建PostTag列表,也進行批量保存,這樣才算是新增好一篇文章,最後輸出ResponseText.INSERT_SUCCESS常量,提示成功。

BlogController.Admin.cs添加API。

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> InsertPostAsync([FromBody] EditPostInput input)
{
    return await _blogService.InsertPostAsync(input);
}

更新文章

更新操作和新增操作輸入參數一樣,只新增一個Id用來標識更新那篇文章,添加UpdatePostAsync更新文章接口。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
Task<ServiceResult> UpdatePostAsync(int id, EditPostInput input);

同樣的實現這個接口。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ServiceResult> UpdatePostAsync(int id, EditPostInput input)
{
    var result = new ServiceResult();

    var post = await _postRepository.GetAsync(id);
    post.Title = input.Title;
    post.Author = input.Author;
    post.Url = $"{input.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{input.Url}/";
    post.Html = input.Html;
    post.Markdown = input.Markdown;
    post.CreationTime = input.CreationTime;
    post.CategoryId = input.CategoryId;

    await _postRepository.UpdateAsync(post);

    var tags = await _tagRepository.GetListAsync();

    var oldPostTags = from post_tags in await _postTagRepository.GetListAsync()
                      join tag in await _tagRepository.GetListAsync()
                      on post_tags.TagId equals tag.Id
                      where post_tags.PostId.Equals(post.Id)
                      select new
                      {
                          post_tags.Id,
                          tag.TagName
                      };

    var removedIds = oldPostTags.Where(item => !input.Tags.Any(x => x == item.TagName) &&
                                               tags.Any(t => t.TagName == item.TagName))
                                .Select(item => item.Id);
    await _postTagRepository.DeleteAsync(x => removedIds.Contains(x.Id));

    var newTags = input.Tags
                       .Where(item => !tags.Any(x => x.TagName == item))
                       .Select(item => new Tag
                       {
                           TagName = item,
                           DisplayName = item
                       });
    await _tagRepository.BulkInsertAsync(newTags);

    var postTags = input.Tags
                        .Where(item => !oldPostTags.Any(x => x.TagName == item))
                        .Select(item => new PostTag
                        {
                            PostId = id,
                            TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id
                        });
    await _postTagRepository.BulkInsertAsync(postTags);

    result.IsSuccess(ResponseText.UPDATE_SUCCESS);
    return result;
}

ResponseText.UPDATE_SUCCESS是常量更新成功。

先根據Id查詢到數據庫中的這篇文章數據,然後根據input參數,修改需要修改的數據,最後保存。

注意的是,如果修改的時候修改了標籤,有可能新增也有可能刪除,也許會又有新增又有刪除。

這時候就需要注意,這裏做了一個比較通用的方法,找到數據庫中當前文章Id的所有Tags,然後根據參數input.Tags可以找出被刪掉的標籤的PostTags的Id,調用刪除方法刪掉即可,同時也可以獲取到新增的標籤,批量進行保存。

完成上面操作后,才保存新加標籤與文章對應的數據,最後提示更新成功,在BlogController.Admin添加API。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
[HttpPut]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> UpdatePostAsync([Required] int id, [FromBody] EditPostInput input)
{
    return await _blogService.UpdatePostAsync(id, input);
}

[HttpPut]指定請求方式為put請求,一般需要修改用put,添加用post。

[Required]指定參數id必填且是FromQuery的方式,input為[FromBody]

更新一下上面新增的數據試試。

刪除文章

刪除相對來說就非常簡單了,一般刪除都會做邏輯刪除,就是避免某些手殘刪除了,有找回的餘地,我們這裏就直接Delete了,也沒什麼重要數據。

添加接口:DeletePostAsync

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<ServiceResult> DeletePostAsync(int id);

實現接口。

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<ServiceResult> DeletePostAsync(int id)
{
    var result = new ServiceResult();

    var post = await _postRepository.GetAsync(id);
    if (null == post)
    {
        result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("Id", id));
        return result;
    }

    await _postRepository.DeleteAsync(id);
    await _postTagRepository.DeleteAsync(x => x.PostId == id);

    result.IsSuccess(ResponseText.DELETE_SUCCESS);
    return result;
}

刪除的時候同樣去查詢一下數據,來判斷是否存在。

ResponseText.DELETE_SUCCESS是添加的常量刪除成功,刪除成功同時也要將post_tags表的標籤對應關係也幹掉才算完整,在BlogController.Admin添加API。

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpDelete]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> DeletePostAsync([Required] int id)
{
    return await _blogService.DeletePostAsync(id);
}

[HttpDelete]指定請求方式是刪除資源,[Required]指定參數Id必填。

刪掉上面添加的文章看看效果。

至此,完成了博客文章的增刪改接口,未完待續…

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

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

想不到7.5萬起車型開起來比14萬的車還更爽!

38-11。28萬本田飛度有着“知乎神車”的稱號,原因10萬以內沒能找到一輛這樣低油耗、加速快以及高保值率的合資車。它使用的是1。5L地球夢發動機,最大功率96千瓦,配合5MT手動變速箱,可以實現9s以內的百公里加速時間,另外還有CVT變速箱可以選擇。

前言

“才六七萬錢的車,能開就行了,還要什麼操控樂趣?”,相信這句話是不少人都聽到過的,認為低價位的汽車都是比較無聊的,但是有沒有想過有些六七萬起售的汽車能給到你超越價格的樂趣,甚至要比貴了一倍價錢的車型還要好玩?

小型車,其實是一個被忽略的市場,在國產緊湊型轎車以及小型SUV的轟炸下,合資小型車的市場份額被逐漸蠶食。以前那種合資小型車百花齊放的景象已經看不到了,前些年年輕人都追求的是這種有着絕佳的操控樂趣小車。

它們都有着小巧的車身,在車流中可以非常靈活地穿梭;有着精準的操控,坐在車內有着一種人車合一的感覺;有着極低的重心,即使在山道也能有着較佳的車身姿態。有着類似卡丁車的操控樂趣,而且還有着較高的改裝潛力。

上汽大眾-pOLO

官方指導價:7.59-14.69萬

作為德系的代表,大眾pOLO有着極高的造詣,小巧的車身配合精準的操控,這輛小型車有着超高的駕駛樂趣,較高速度過彎都能有着一個極佳的姿態。所以在不少車隊甚至是大眾官方都是將其作為賽車車型的首選,價格低操控好而且改裝潛力大。

不過對於家庭用戶來說,後排空間就相對局促。動力方面有着1.4L以及1.6L自然吸氣發動機可以選擇,變速箱則是5MT以及6AT變速箱可以選擇,不過更推薦的是5MT,傳動效率高動力佳,而且檔位清晰,離合容易控制,上手難度低。

廣汽本田-飛度

官方指導價:7.38-11.28萬

本田飛度有着“知乎神車”的稱號,原因10萬以內沒能找到一輛這樣低油耗、加速快以及高保值率的合資車。它使用的是1.5L地球夢發動機,最大功率96千瓦,配合5MT手動變速箱,可以實現9s以內的百公里加速時間,另外還有CVT變速箱可以選擇。

它還有着極其優秀的車廂空間,完全不亞於某些合資緊湊型轎車,加上低油耗的特點非常適合家庭使用。動力強以及手感極好的手動變速箱,讓它有着超高的加速性能,不過過彎的時候側傾稍微有點大,但幸好可以通過改裝改善。

長安鈴木-雨燕

官方指導價:5.98-8.28萬

鈴木雨燕是之前小型車大熱時的產物,到如今那麼多年都僅僅是對外觀進行小小的修改,所以有着當初那種最佳的駕駛樂趣,不過也是因為這麼多年都沒有進行性能上的改進而為人所詬病。發動機方面有着1.3L以及1.5L兩種排量可以選擇,1.5L自然吸氣發動機最大功率76千瓦,相對弱勢。

但配合5MT手動變速箱操控極佳,加上優秀的底盤表現以及改裝潛力使得它是第三代飛度之前最熱的改裝車,如今在不少賽車場都可以看到它的蹤影。並且對於某些改裝發燒友來說,可以無損改裝進口速翼特的M16A發動機也是重大的優勢,動力輸出更加,高轉有着極其優秀的表現和魅力。

編輯總結:

以上三款汽車都可以說是這個價位中操控最好的代表,甚至比起12萬左右的合資緊湊型轎車還要優秀。價格基本可以在不到7萬可以買到,尤其是大眾pOLO,終端起售價甚至可以在6.5萬左右,非常適合追求駕駛樂趣的年輕人。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

完爆繽智逍客之流,這輛超低油耗SUV能把人帥暈

加上脫胎於豐田普銳斯平台以及前後獨立懸架使得它駕駛質感就像是一輛兩廂車。無論是1。8L混動版本還是1。2T渦輪增壓版本,駕駛起來都是非常的輕快,1。8L混動版本在溫柔的駕駛風格有着最為安靜的駕駛體驗,但若是你以一種激進的方式去駕駛這輛車,發動機介入后的轟鳴就會從防火牆一直傳遞到你的耳朵,所以我們還是建議溫柔一點開。

前言

合資小型SUV市場,基本可以認為是本田繽智、H-RV兩兄弟以及日產逍客所佔領,其他車系就僅僅有着昂科拉以及yeti這些車型,完全不足以向它們發起衝鋒。而豐田在這個競爭激烈的小型SUV卻一直沒有參与,但是“遲到總比沒到好”,豐田的最新小型SUV——CH-R似乎有着超強的競爭實力。

豐田C-HR是一輛為年輕人所設計的小型SUV,在外觀極其動感,使用了家族式Keen-look設計原因,修長的鷹眼大燈非常有進攻性,整體就像是一輛小RAV4。

鷹眼式大燈非常引人注目,使用的是氙氣大燈,下方的是閃電狀LED日間行車燈,年輕人對這種設計的抵禦能力幾乎是“零”。

車身側面可以看到兩個突出的輪轂,並且雙腰線的設計,還有一條從前輪一直延伸到尾燈處,將車輪、車門、尾燈連成一體,充分表現出力量感。並且使用了溜背+懸浮式車頂設計,動感十分。值得一提的是為了不破壞車身側麵線條,後門把手使用了隱藏式設計,位置是在車門的右上角。

尾部使用的是雙C型尾燈,和本田思域非常類似。后擋風玻璃傾斜程度很大,使得這輛小型SUV更像是一輛性感的掀背車。

尾部還有稍微地突出,有着一種小鴨尾的感覺。

尾燈燈源使用的是LED燈源,不僅美觀,還有着很高的辨識率。

內飾也是和外觀一般驚艷,是豐田最新的設計,做工也是豐田最高的標準,值得稱讚。中控有着裝飾條從一側車門延伸到另一側。懸浮式中控屏幕位置較高便於使用,但是稍顯突兀。

飛機把桿式的空調按鈕頗有Mini的感覺。

檔把也是全新的設計,後方有着电子手剎、自動駐車、EV模式以及ESp車身穩定系統開關。

CH-R的項目負責人介紹道CH-R為了打造這外觀,加上認為目標用戶基本不使用後排,所以犧牲了後排空間以及行李廂容積,而且後排不僅較小,而且視野極差。

C-HR的動感不僅僅表現在外觀,也表現在操控上,有着一個精準的轉向。雖然它看着較高,但是比起普銳斯更短更寬的車身以及優秀的設計使得它有着較低的重心。加上脫胎於豐田普銳斯平台以及前後獨立懸架使得它駕駛質感就像是一輛兩廂車。

無論是1.8L混動版本還是1.2T渦輪增壓版本,駕駛起來都是非常的輕快,1.8L混動版本在溫柔的駕駛風格有着最為安靜的駕駛體驗,但若是你以一種激進的方式去駕駛這輛車,發動機介入后的轟鳴就會從防火牆一直傳遞到你的耳朵,所以我們還是建議溫柔一點開。而且混動版本那讓人捉摸不透的剎車也是一個問題。

對於1.2T版本,手動變速箱檔位清晰,能帶來更多的駕駛樂趣,當然你也可以選擇CVT版本。這個1.2T發動機動力表現比起1.6L自然吸氣發動機要更好,已經在我國實現了量產,裝載到雷凌以及卡羅拉上,唯獨是5500轉的斷油轉速實在是有點低。

H-RV在主動安全配置方面也是有着較高的水準,ACC自適應巡航、全車影像、車道偏離預警、主動剎車均有配備。

在中國市場乃至是整個世界,豐田C-HR的主要對手是本田繽智、日產逍客、斯柯達yeti這類的小型SUV,進入中國的價位極有可能在13-20萬之間,這次C-HR的亮相已經給到對手明顯的競爭壓力。相比之下,繽智之流的SUV在顏值落下了不止了一個層級,其次是主動安全配置。

本田有着思域這款“高性價比”以及驚艷的產品,而豐田此次的C-HR也同樣驚艷,而且加上1.2T發動機、混動動力總成的國產化,價格肯定也是相當的低。不過如此優秀的產品是否會加價呢?

這個問題已經在編輯部引起了強烈的爭議,有人認為如此優秀的產品不加價是沒有可能的,有人認為豐田慣例不會加價只是讓你等,也有人認為這輛後排空間小的車會比較小眾,所以不會加價。那麼,你是怎樣看的呢?要是真的13萬起售,你會選擇加價提車嗎?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

奇瑞全新小型SUV瑞虎3x智趣上市 售價5.89-8.09萬元

9L,燃油經濟性十分突出。瑞虎3x大量選用環保材料技術,在零件、系統、整車各個階段對內飾空氣質量層層監控,營造綠色、環保、低氣味的內飾空間。同時,瑞虎3x遵循五星安全標準設計,具有高強度車身結構和嚴謹的製造工藝,不僅擁有輕量化前防撞鋼樑、環護式四位一體安全氣囊的全方位被動安全配置,還配備了全方位倒車雷達+動態輔助倒車影像系統、全新ESC車身穩定系統、TpMS直接式胎壓監測、EBA剎車輔助控制系統、上坡輔助HHC等越級高科技主動安全配置,全方位保障用戶行車安全。

11月15日,奇瑞全新小型SUV—瑞虎3x正式上市。新車共推出搭載1.5VVT發動機共10款車型,售價5.89萬——8.09萬元。

瑞虎3x是奇瑞戰略2.0體系下的第三款全新產品,將以“同級首款4G互聯SUV”的優勢進軍小型SUV市場,並與此前上市的新瑞虎3共同組成產品雙擎,進一步完善瑞虎家族產品線,實現對年輕時尚細分市場的全面覆蓋。

以“智趣”為標籤的瑞虎3x是奇瑞基於當下年輕消費群體的用車需求以及SUV年輕化、智能化、個性化的發展趨勢,為追求獨特個性、玩樂精神的年輕群體量身打造的一款“4G智趣SUV”。

“全新品質標杆” 引領小型SUV新潮流

作為奇瑞2.0產品的新一代小型SUV,無論是引領潮流的造型設計,還是智能互聯的科技屬性,都讓瑞虎3x領先一步樹立起小型SUV市場的全新品質標杆。

雖然是一款體量小的SUV,但瑞虎3x在尺寸空間和動力性能上都有獨特優勢。4200 *1760 *1570 mm的車身尺寸和2555 mm的超長軸距全面超越同級競爭對手。合理的設計使瑞虎3x在實際空間的表現上更為突出,提供了同級別最優的前排頭腿部空間、後排頭部空間,高達420L的後備箱載物空間也是同級容積最大。升級后的1.5VVT全國十佳發動機匹配5MT/4AT兩款變速箱,使瑞虎3x的最大輸出功率高達106馬力。百公里40m制動距離、186mm的最小離地間隙、24°接近角、32°離去角更是造就了瑞虎3x的同級最優通過性。

憑藉奇瑞2.0體系的強大支撐,瑞虎3x的品質和可靠性比肩國際水準。在耐久可靠性方面,瑞虎3x經歷了全球極限環境的苛刻路試,通過了近100項試驗驗證,開發過程中總計投入試驗車超過200台,累計行駛里程數約110萬公里;百公里綜合油耗僅5.9L,燃油經濟性十分突出。瑞虎3x大量選用環保材料技術,在零件、系統、整車各個階段對內飾空氣質量層層監控,營造綠色、環保、低氣味的內飾空間。同時,瑞虎3x遵循五星安全標準設計,具有高強度車身結構和嚴謹的製造工藝,不僅擁有輕量化前防撞鋼樑、環護式四位一體安全氣囊的全方位被動安全配置,還配備了全方位倒車雷達+動態輔助倒車影像系統、全新ESC車身穩定系統、TpMS直接式胎壓監測、EBA剎車輔助控制系統、上坡輔助HHC等越級高科技主動安全配置,全方位保障用戶行車安全。在售後服務方面也擁有尊貴禮遇,可享受“3年10萬公里整車保修”的服務品質保障。

“4G全時互聯” 定義全新用車生活

對以“智趣”為靈魂的瑞虎3x來說,“4G全時互聯”的引入,成為瑞虎3x最受關注之處。在智能科技配置方面,瑞虎3x深諳年輕用戶對互聯的需求,搭載了Cloudrive2.0智雲互聯行車系統,不僅配備了4G極速wifi、VOS智能語音控制、語音導航、Carplay手機互聯、8寸超大高清彩屏、動態輔助倒車影像、GSI換擋提醒等多項同級別獨有配置,還配有定速巡航、伴我回家功能、后視鏡電加熱 、外后視鏡集成轉向燈、遙控鑰匙等人性化科技配置,樹立起小型SUV配置標杆。

汽車“雲”服務是未來汽車發展的趨勢,瞄準年輕用戶的瑞虎3x,此次聯手業界“語音怪獸”科大訊飛推出Cloudrive 2.0智能語音交互系統,是同級唯一擁有4G互聯功能的SUV。這套系統此前在艾瑞澤5車型上已經體現了強大的功能和互動便利性,此次更是不惜成本在原先版本上升級優化,打造出“中國最強”車載語音系統。

“能說話絕不動手”,瑞虎3x還突破性地迎來了人機之間的無障礙溝通。系統內置4G無線網卡、硬件採用2G DDR3/8G ROM雙核,可實現最快的瀏覽分享速度;雙麥語音降噪模塊、雲+端語音識別技術的應用,普通話識別率近100%,16種方言也能精準識別交互;On Cloud全網雲端在線服務,私人定製在線服務、海量數據支持等互聯體驗使人與機器的交流像人與人之間的“對話”,一舉顛覆傳統用車方式。

2.0發力 品牌年輕化築優勢

從小型SUV瑞虎3到緊湊型SUV瑞虎5,再到中級SUV瑞虎7……瑞虎3x的上市,將進一步完成奇瑞在SUV市場的全方位布局。瑞虎3x將與新瑞虎3一起組合“雙子星“,實現奇瑞在SUV市場實現更細分的市場覆蓋,形成個性鮮明、高低搭配的瑞虎家族產品矩陣。

從今年上市的艾瑞澤5、瑞虎7、瑞虎3x這三款奇瑞2.0產品可以看到,奇瑞在設計中摒棄了中庸的設計理念,採用了大量時尚、個性的元素,充分迎合年輕消費者的審美和喜好。在科技配置上,新一代車型增強了智能互聯功能,搭載了語音交互、雲端服務、內置4G網卡等配置,滿足年輕消費群體高度依賴互聯網的生活方式和出行需求。奇瑞把未來品牌和產品的主力目標人群鎖定為年輕人,品牌形象正在變得更加年輕、時尚、國際范。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準