一文說通Dotnet Core的後台任務

這是一文說通系列的第二篇,裏面有些內容會用到第一篇中間件的部分概念。如果需要,可以參看第一篇:一文說通Dotnet Core的中間件

一、前言

後台任務在一些特殊的應用場合,有相當的需求。

比方,我們需要實現一個定時任務、或周期性的任務、或非API輸出的業務響應、或不允許併發的業務處理,像提現、支付回調等,都需要用到後台任務。

通常,我們在實現後台任務時,有兩種選擇:WebAPI和Console。

下面,我們會用實際的代碼,來理清這兩種工程模式下,後台任務的開發方式。

    為了防止不提供原網址的轉載,特在這裏加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13081020.html

二、開發環境&基礎工程

這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.201
 Commit:    b1768b4ae7

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/3.1.201/

Host (useful for support):
  Version: 3.1.3
  Commit:  4a9f85e9f8

.NET Core SDKs installed:
  3.1.201 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

首先,在這個環境下建立工程:

  1. 創建Solution
% dotnet new sln -o demo
The template "Solution File" was created successfully.
  1. 這次,我們用Webapi創建工程
cd demo
% dotnet new webapi -o webapidemo
The template "ASP.NET Core Web API" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on webapidemo/webapidemo.csproj...
  Restore completed in 179.13 ms for demo/demo.csproj.

Restore succeeded.
% dotnet new console -o consoledemo
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on consoledemo/consoledemo.csproj...
  Determining projects to restore...
  Restored consoledemo/consoledemo.csproj (in 143 ms).

Restore succeeded.
  1. 把工程加到Solution中
% dotnet sln add webapidemo/webapidemo.csproj
% dotnet sln add consoledemo/consoledemo.csproj

基礎工程搭建完成。

三、在WebAPI下實現一個後台任務

WebAPI下後台任務需要作為託管服務來實現,而託管服務,需要實現IHostedService接口。

首先,我們需要引入一個庫:

cd webapidemo
% dotnet add package Microsoft.Extensions.Hosting

引入后,我們就有了IHostedService

下面,我們來做一個IHostedService的派生託管類:

namespace webapidemo
{
    public class DemoService : IHostedService
    {
        public DemoService()
        
{
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            throw new NotImplementedException();
        }

        public Task StopAsync(CancellationToken cancellationToken)
        
{
            throw new NotImplementedException();
        }
    }
}

IHostedService需要實現兩個方法:StartAsyncStopAsync。其中:

StartAsync: 用於啟動後台任務;

StopAsync:主機Host正常關閉時觸發。

如果派生類中有任何非託管資源,那還可以引入IDisposable,並通過實現Dispose來清理非託管資源。

這個類生成后,我們將這個類注入到ConfigureServices中,以使這個類在Startup.Configure調用之前被調用:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<DemoService>();
}

下面,我們用一個定時器的後台任務,來加深理解:

namespace webapidemo
{
    public class TimerService : IHostedService, IDisposable
    {
          /* 下面這兩個參數是演示需要,非必須 */
        private readonly ILogger _logger;
        private int executionCount = 0;

          /* 這個是定時器 */
        private Timer _timer;

        public TimerService(ILogger<TimerService> logger)
        
{
            _logger = logger;
        }

        public void Dispose()
        
{
            _timer?.Dispose();

        }

        private void DoWork(object state)
        
{
            var count = Interlocked.Increment(ref executionCount);

            _logger.LogInformation($"Service proccessing {count}");
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service starting");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service stopping");

            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }
    }
}

注入到ConfigureServices中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<TimerService>();
}

就OK了。代碼比較簡單,就不解釋了。

四、WebAPI後台任務的依賴注入變形

上一節的示例,是一個簡單的形態。

下面,我們按照標準的依賴注入,實現一下這個定時器。

依賴注入的簡單樣式,請參見一文說通Dotnet Core的中間件。

首先,我們創建一個接口IWorkService

namespace webapidemo
{
    public interface IWorkService
    {
        Task DoWork();
    }
}

再根據IWorkService,建立一個實體類:

namespace webapidemo
{
    public class WorkService : IWorkService
    {
        private readonly ILogger _logger;
        private Timer _timer;
        private int executionCount = 0;

        public WorkService(ILogger<WorkService> logger)
        
{
            _logger = logger;
        }

        public async Task DoWork()
        
{
            var count = Interlocked.Increment(ref executionCount);

            _logger.LogInformation($"Service proccessing {count}");
        }
    }
}

這樣就建好了依賴的全部內容。

下面,創建託管類:

namespace webapidemo
{
    public class HostedService : IHostedService, IDisposable
    {
        private readonly ILogger<HostedService> _logger;
        public IServiceProvider Services { get; }
        private Timer _timer;

        public HostedService(IServiceProvider services, ILogger<HostedService> logger)
        
{
            Services = services;
            _logger = logger;
        }

          public void Dispose()
        
{
            _timer?.Dispose();
        }

        private void DoWork(object state)
        
{
            _logger.LogInformation("Service working");

            using (var scope = Services.CreateScope())
            {
                var scopedProcessingService =
                    scope.ServiceProvider
                        .GetRequiredService<IWorkService>();

                scopedProcessingService.DoWork().GetAwaiter().GetResult();
            }
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service starting");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
            return Task.CompletedTask;
        }


        public Task StopAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service stopping");

            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }
    }
}

把託管類注入到ConfigureServices中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<HostedService>();
    services.AddSingleton<IWorkService, WorkService>();
}

這樣就完成了。

這種模式下,可以根據注入的內容切換應用的執行內容。不過,這種模式需要注意services.AddSingletonservices.AddScopedservices.AddTransient的區別。

五、Console下的後台任務

Console應用本身就是後台運行,所以區別於WebAPI,它不需要託管運行,也不需要Microsoft.Extensions.Hosting庫。

我們要做的,就是讓程序運行,就OK。

下面是一個簡單的Console模板:

namespace consoledemo
{
    class Program
    {

        private static AutoResetEvent _exitEvent;

        static async Task Main(string[] args)
        
{
                /* 確保程序只有一個實例在運行 */
            bool isRuned;
            Mutex mutex = new Mutex(true"OnlyRunOneInstance", out isRuned);
            if (!isRuned)
                return;

            await DoWork();

                        /* 後台等待 */
            _exitEvent = new AutoResetEvent(false);
            _exitEvent.WaitOne();
        }

        private static async Task DoWork()
        
{
            throw new NotImplementedException();
        }
    }
}

這個模板有兩個關鍵的內容:

  1. 單實例運行:通常後台任務,只需要有一個實例運行。所以,第一個小段,是解決單實例運行的。多次啟動時,除了第一個實例外,其它的實例會自動退出;
  2. 後台等待:看過很多人寫的,在這兒做後台等待時,用了一個無限的循環。類似於下面的:
while(true)
{
    Thread.Sleep(1000);
}

這種方式也沒什麼太大的問題。不過,這段代碼總是要消耗CPU的計算量,雖然很少,但做為後台任務,或者說Service,畢竟是一種消耗,而且看着不夠高大上。

當然如果我們需要中斷,我們也可以把這個模板改成這樣:

namespace consoledemo
{
    class Program
    {

        private static AutoResetEvent _exitEvent;

        static async Task Main(string[] args)
        
{
            bool isRuned;
            Mutex mutex = new Mutex(true"OnlyRunOneInstance", out isRuned);
            if (!isRuned)
                return;

            _exitEvent = new AutoResetEvent(false);
            await DoWork(_exitEvent);
            _exitEvent.WaitOne();
        }

        private static async Task DoWork(AutoResetEvent _exitEvent)
        
{
            /* Your Code Here */

            _exitEvent.Set();
        }
    }
}

這樣就可以根據需要,來實現中斷程序並退出。

六、Console應用的其它運行方式

上一節介紹的Console,其實是一個應用程序。

在實際應用中,Console程序跑在Linux服務器上,我們可能會有一些其它的要求:

  1. 定時運行

Linux上有一個Service,叫cron,是一個用來定時執行程序的服務。

這個服務的設定,需要另一個命令:crontab,位置在/usr/bin下。

具體命令格式這兒不做解釋,網上隨便查。

  1. 運行到後台

命令後邊加個&字符即可:

$ ./command &
  1. 運行為Service

需要持續運行的應用,如果以Console的形態存在,則設置為Service是最好的方式。

Linux下,設置一個應用為Service很簡單,就這麼簡單三步:

第一步:在/etc/systemd/system下面,創建一個service文件,例如command.service

[Unit]
# Service的描述,隨便寫
Description=Command

[Service]
RestartSec=2s
Type=simple
# 執行應用的默認用戶。應用如果沒有特殊要求,最好別用root運行
User=your_user_name
Group=your_group_name
# 應用的目錄,絕對路徑
WorkingDirectory=your_app_folder
# 應用的啟動路徑
ExecStart=your_app_folder/your_app
Restart=always

[Install]
WantedBy=multi-user.target

差不多就這麼個格式。參數的詳細說明可以去網上查,實際除了設置,就是運行了一個腳本。

第二步:把這個command.service加上運行權限:

# chmod +x ./command.service

第三步:註冊為Service:

# systemctl enable command.service

完成。

為了配合應用,還需要記住兩個命令:啟動和關閉Service

# #啟動Service
# systemctl start command.service
# #關閉Service
# systemctl stop command.service

七、寫在後邊的話

今天這個文章,是因為前两天,一個兄弟跑過來問我關於數據總線的實現方式,而想到的一個點。

很多時候,大家在寫代碼的時候,會有一種固有的思想:寫WebAPI,就想在這個框架中把所有的內容都實現了。這其實不算是一個很好的想法。WebAPI,在業務層面,就應該只是實現簡單的處理請求,返回結果的工作,而後台任務跟這個內容截然不同,通常它只做處理,不做返回 — 事實上也不太好返回,要麼客戶端等待時間太長,要麼客戶端已經斷掉了。換句話說,用WebAPI實現總線,絕不是一個好的方式。

不過,Console運行為Service,倒是一個總線應用的絕好方式。如果需要按序執行,可以配合MQ服務器,例如RabbitMQ,來實現消息的按序處理。

再說代碼。很多需求,本來可以用很簡單的方式實現。模式這個東西,用來面試,用來講課,都是很好的內容,但實際開發中,如果有更簡單更有效的方式,用起來!Coding的工作是實現,而不是秀技術。當然,能否找到簡單有效的方式,這個可能跟實際的技術面有關係。但這並不是一個不能跨越的坎。

多看,多想,每天成長一點點!

今天的代碼,在:https://github.com/humornif/Demo-Code/tree/master/0012/demo

(全文完)

 

 

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

不忽悠直接試!這輛車為什麼讓很多人慾罷不能?

對於在城市裡比較緊張的停車場停車,這就是避免剮蹭的一大保障。不過通過後視攝像頭集成倒車影像在這個價位的車裡不算特別稀奇,新凌派的安全配置才是最大的亮點。全系標配VSA車身穩定控制系統、HSA斜坡起步輔助系統、ESS緊急剎車警示系統及領先搭載LWC盲點显示系統四大安全科技。

又到年底,一年中大家的荷包最豐厚的時節,於是買車成為了不少人的心頭大事。後台關於車型的詢問量也與日俱增,搞得最近黃金右手有點撐不住。正好某天看到有個粉絲問能不能說說凌派,更妙的是恰好咱們手上有輛凌派試駕車,與其用黃金右手碼字說,不如直接上黃金右腳來得直接。於是一不做二不休乾脆勾搭上這位粉絲,帶着他共同試駕新凌派,讓他親自感受一番這車究竟如何!

這一次我們試駕的車型是次頂配,香檳雅金的凌派,這是新凌派的主打車色,路上見到的也多,可見大家對“土豪金”的喜愛。

接着另一位主角登場,正是邀請來試駕的粉絲——阿聰。

外觀——“時尚中不失個性”

阿聰首先感受了一番新凌派的外觀,他直言自己算是個“顏控”。而且相比中規中矩的卡羅拉、雷凌,他更喜歡凌派“時尚中不失個性”的外觀。

覺得,阿聰這個描述很到位。如果把同級車型比成一個班級,凌派就是班級里打扮最前衛,每次出現在教室門口全班同學都會抬頭看的那位。前臉十分特別,“大嘴”前格柵與“大眼睛”一般的大燈組成了獨特的“笑模樣”。側面刀鋒式的雙腰線和切削式的鋁合金輪轂造型充分展現個性,尾部採用運動型保險杠體現動感。可以說新凌派絕對是一輛辨識度非常高的車,恰恰滿足了像阿聰這樣的年輕人不想“泯然眾人已”的心理。

內飾——“簡約而不簡單”

進入車內,空調出風口、中控大屏、各個功能按鍵有序分佈,整體設計是一貫的本田風格,簡潔、實用、時尚。內飾用料也比較精細,手觸摸上去軟硬適中,完全沒有塑料感,相比很多同級車型,新凌派在這個價位做出了質感,這是非常讓人驚喜的地方。

在給阿聰對內飾做了一番講解后,阿聰瞭然的點頭,一句話總結了要表達的意思:“簡約而不簡單。”他想要的也正是這樣內飾布局簡單明了,一坐進去就非常容易上手操作的車。

空間——“夠大夠舒服”

體驗了內飾后,問阿聰對車的空間有些什麼要求。阿聰說主要是平時上下班代步,去運動,周末和朋友短途自駕,所以乘坐空間要夠大夠舒服,後備箱最好能多裝點。聽完就覺得難怪他會偏愛新凌派了。

擁有4664mm的超長車身和2650mm的軸距優勢,加上本田大師水準的空間利用技術,及本田獨有的MM理念的運用,新凌派無論前後排空間都非常寬敞。為了讓阿聰更加深刻的體驗到新凌派的空間,上陣教了他一套“媒體老濕”用的空間測試法。阿聰身高175,坐進新凌派後頭部空間一拳半,腿部空間遠遠超過兩拳,完全能夠舒服的乘坐。

後備箱空間就更能滿足阿聰的需求了。無論深度還是寬度都很夠,放了一摞又一摞物料后,阿聰整個人還能愜意的躺進後備箱,這個展現後備箱寬敞程度的招式,也是服氣。

配置——“亮點滿滿”

當然阿聰更關心的還有凌派的配置。上車之前他就親自體驗了新凌派的無鑰匙進入系統,只要攜帶鑰匙靠近,車門就會自動解鎖。阿聰覺得這個功能非常酷。

而從車內分佈的各個按鍵也能知道配置的豐富程度。尤其一鍵啟停、小綠恭弘=叶 恭弘也深得阿聰這個90后的歡心,既體現高科技,又保證經濟性。

細心的阿聰還發現,新凌派配備的倒車影像具有三模式視角,分別是廣角、常規和高角度。對於在城市裡比較緊張的停車場停車,這就是避免剮蹭的一大保障。

不過通過後視攝像頭集成倒車影像在這個價位的車裡不算特別稀奇,新凌派的安全配置才是最大的亮點。全系標配VSA車身穩定控制系統、HSA斜坡起步輔助系統、ESS緊急剎車警示系統及領先搭載LWC盲點显示系統四大安全科技。而LWC盲點显示系統更是本田的“黑科技”代表之一,在凌派的同級車型中更是絕無僅有。

LWC盲點显示系統使用非常簡單,只要按一下方向盤後方擋把上的啟動按鈕,就能啟動位於右後視鏡上的攝像頭,右後方的影像立即显示在中控大屏上。啟動這個功能后,右後方的視野能從20%擴大到80%,消除肉眼觀察時的盲區,並且帶有輔助線以供駕駛者判斷安全距離。

對這個逆天的功能阿聰表示,開車時右後方經常會有視線盲區,尤其在複雜路況行車時,切線或者轉彎都不免讓很多人的心都提一提,現在只需輕輕看一眼就能掌握右後方路況,開起車來會從容許多。甚至平時還可以來個創意自拍,新凌派的配置不僅豐富,還亮點滿滿。

只想說:果然是年輕人,會玩!

動力——“舒適之餘也談激情”

動力上凌派搭載的是1.8L自然吸氣發動機,擁有本田看家的i-VTEC系統,匹配CVT無級變速器,最大功率100kW,最大扭矩169N·m,參數表現是非常不錯的。而阿聰體驗過後實際是什麼感受呢?

剛把車發動起來,阿聰就感受到凌派強勁的動力了。不同於很多車經常會有起步比較“肉”的感受,本田的可變氣門正時系統讓發動機即使在低扭矩時也能保證足夠的動力輸出。

一天的相處下來,發現阿聰是一個個性又不失內斂的男生,開車風格是“暖男派”,不爭不搶。在市區駕駛時開啟ECON模式是非常適合他的,兼顧舒適性和經濟性。而當他偶爾想要來點激情時,讓他切到S擋,深踩油門感受了一番新凌派的加速,油門與剎車響應都比較靈敏,阿聰開起來完全游刃有餘。

最後還帶着阿聰來了一次劈彎。在比較高速的情況下轉彎,凌派的側傾只會讓你恰到好處的感覺到車身在過彎,並不會有傾斜幅度過大的感覺,車尾的循跡性也不錯。可見新凌派底盤是主打舒適的調校。用阿聰的話來說,新凌派給他的感覺非常“穩”。

最終一天試駕下來的油耗也讓阿聰更“穩”了,他開出了百公里6.8L的超低油耗,看來黃金右腳的稱號要拱手讓人了。

分別前問了阿聰最後一個,也是試駕前阿聰本人帶來的問題:

“今天能不能讓你決定射出這臨門一腳買下凌派呢?”

阿聰回答說:

“本來就覺得新凌派很對胃口,在和一起試駕后,還發現了它獨特的LWC、強勁的動力、超大的空間等超越同級的亮點,可以說由內而外都非常適合我,一天下來我感覺和新凌派幾乎不用磨合,不禁產生一種“這就是我的車”的感覺。”

還是那句話,最適合你的車就是最好的車。像阿聰這樣的年輕人,一輛外觀時尚,配置豐富,大空間,動力足的十萬級別轎車就是最大的助力。而擁有1.5油耗,1.6價格,1.8動力,2.0空間的新凌派,阿聰試完之後已經找不出不買它的理由。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

@Autowired 註解詳解

前言

我們平時使用 Spring 時,想要 依賴注入 時使用最多的是 @Autowired 註解了,本文主要講解 Spring 是如何處理該註解並實現 依賴注入 的功能的。

正文

首先我們看一個測試用例:

User 實體類:

public class User {

    private Long id;
    private String name;

	// 省略 get 和 set 方法
}

測試類:

public class AnnotationDependencyInjectTest {

    /**
     * @Autowired 字段注入
     */
    @Autowired
    private User user;

    private City city;

    /**
     * @Autowired 方法注入
     */
    @Autowired
    public void initCity(City city) {
        this.city = city;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AnnotationDependencyInjectTest.class);
        context.refresh();
        AnnotationDependencyInjectTest bean = context.getBean(AnnotationDependencyInjectTest.class);
        // @Autowired 字段注入
        System.out.println(bean.user);
        // @Autowired 方法注入
        System.out.println(bean.city);
        UserHolder userHolder = context.getBean(UserHolder.class);
        // @Autowired 構造器注入
        System.out.println(userHolder.getUser());
        context.close();
    }

    @Bean
    public User user() {
        User user = new User();
        user.setId(1L);
        user.setName("leisurexi");
        return user;
    }

    @Bean
    public City city() {
        City city = new City();
        city.setId(1L);
        city.setName("北京");
        return city;
    }
    
    /**
     * @Autowired 構造函數注入
     */
    static class UserHolder {

        private User user;

        @Autowired
        public UserHolder(User user) {
            this.user = user;
        }

        public User getUser() {
            return user;
        }

        public void setUser(User user) {
            this.user = user;
        }
    }

}

上面分別展示了 @Autowired 註解的字段注入和方法注入,下面我們開始分析 Spring 是如何實現的。

首先使 @Autowired 註解生效的一個關鍵類是 AutowiredAnnotationBeanPostProcessor,該類實現了 InstantiationAwareBeanPostProcessorAdapter 抽象類;該抽象類就是一個適配器的作用提供了接口方法的默認實現,InstantiationAwareBeanPostProcessorAdapter 又實現了 SmartInstantiationAwareBeanPostProcessor 接口,同時實現該接口的 determineCandidateConstructors() 方法可以指定 bean 的候選構造函數;然後 SmartInstantiationAwareBeanPostProcessor 接口又繼承了 InstantiationAwareBeanPostProcessor 接口,該接口提供了 bean 實例化前後的生命周期回調以及屬性賦值前的後置處理方法,@Autowired 註解的屬性注入就是通過重寫該接口的 postProcessProperties() 實現的。這兩個接口都在 在 Spring IoC createBean 方法詳解 一文中有介紹過。下面我們看一下 AutowiredAnnotationBeanProcessor 的繼承關係圖:

關於 AutowiredAnnotationBeanPostProcessor 這個後置處理器是怎麼加入到 beanFactory 中的,我們在 Spring IoC component-scan 節點詳解 一文中介紹過主要是通過 AnnotationConfigUtils#registerAnnotationConfigProcessors() 實現的。

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
    BeanDefinitionRegistry registry, @Nullable Object source) {
    
    // 忽略其它代碼
    
    // 註冊用於處理@Autowired、@Value、@Inject註解的後置處理器
    if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new
            RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def,
                                           AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
    }
    // 忽略其它代碼
    
}

屬性和方法注入

AutowiredAnnotationBeanPostProcessor 中跟屬性注入有關的方法有兩個:postProcessMergedBeanDefinitionpostProcessPropertyValues

前者是 MergedBeanDefinitionPostProcessor 接口中的方法,定義如下:

public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor {

	/**
	 * 對指定bean的BeanDefinition合併后的處理方法回調
	 */
	void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName);

	/**
	 * @since 5.1
	 * 通知已重新設置指定beanName的BeanDefinition,如果實現該方法應該清除受影響的bean的所有元數據
	 */
	default void resetBeanDefinition(String beanName) {
	}

}

後者是 InstantiationAwareBeanPostProcessor 接口中的方法,定義如下:

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {

	/**
	 * Bean 實例化前調用,返回非 {@code null} IoC 容器不會對 Bean 進行實例化 並且後續的生命周期回調方	  *	法不會調用,返回 {@code null} 則進行 IoC 容器對 Bean 的實例化
	 */
	@Nullable
	default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		return null;
	}

	/**
	 * Bean 實例化之後,屬性填充之前調用,返回 {@code true} 則進行默認的屬性填充步驟,返回 {@code 		 * false} 會跳過屬性填充階段。
	 */
	default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
		return true;
	}

	/**
	 * Bean 實例化后屬性賦值前調用,PropertyValues 是已經封裝好的設置的屬性值,返回 {@code null} 繼續
	 * 使用現有屬性,否則會替換 PropertyValues。
	 * @since 5.1版本新加的和底下的方法一樣
	 */
	@Nullable
	default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
			throws BeansException {
		return null;
	}

	/**
	 * 跟上面方法一樣的功能,只不過是5.1以前版本所使用的
	 * 返回 {@code null} 會跳過屬性填充階段
	 */
	@Deprecated
	@Nullable
	default PropertyValues postProcessPropertyValues(
			PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

		return pvs;
	}

}

關於這兩個方法的調用時機,可以查看 Spring IoC createBean 方法詳解。

AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition

首先執行的是 postProcessMergedBeanDefinition()

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 尋找需要注入的字段或方法,並封裝成 InjectionMetadata,見下文詳解
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    // 檢查元數據中的註解信息
    metadata.checkConfigMembers(beanDefinition);
}

InjectionMetadata 就是注入的元信息描述,主要字段如下:

public class InjectionMetadata {
    
    // 需要依賴注入的目標類
    private final Class<?> targetClass;
    // 注入元素的集合
    private final Collection<InjectedElement> injectedElements;
    
    // 忽略其它代碼
    
}

InjectedElement 就是注入的元素,主要字段如下:

public abstract static class InjectedElement {
    
    // 注入的屬性或方法
    protected final Member member;
    // 需要注入的是否是字段
    protected final boolean isField;
    
}

AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    // Fall back to class name as cache key, for backwards compatibility with custom callers.
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    // 判斷是否需要刷新,即metadata為null或者metadata中存儲的targetClass和當前clazz不等
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        // 這裏相當於是一個double check,防止多線程出現的併發問題
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                // 構建注入元信息,見下文詳解
                metadata = buildAutowiringMetadata(clazz);
                // 放入緩存中
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    // 返回注入元信息
    return metadata;
}

AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
        return InjectionMetadata.EMPTY;
    }
    // 判斷當前類或其字段或其方法是否標註了autowiredAnnotationTypes中的註解,沒有的話直接返回空的
    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
        // 遍歷targetClass中的字段
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            // 獲取field上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(field);
            if (ann != null) {
                // 如果字段是靜態類型是不會進行注入的
                if (Modifier.isStatic(field.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static fields: " + field);
                    }
                    return;
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將裝成AutowiredFieldElement添加進currElements
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });
        // 遍歷targetClass中的方法
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            // 找到橋接方法
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            // 判斷方法的可見性,如果不可見則直接返回
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            // 獲取method上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                // 如果是靜態方法是不會進行注入的
                if (Modifier.isStatic(method.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static methods: " + method);
                    }
                    return;
                }
                // 方法注入沒有參數就違背了初衷,就是在脫褲子放屁
                if (method.getParameterCount() == 0) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " + method);
                    }
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將方法和目標類型封裝成屬性描述符
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                // 封裝成AutowiredMethodElement添加進currElements
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });
        // 將currElements整個添加進elements
        elements.addAll(0, currElements);
        // 獲取targetClass的父類,進行下一次循環
        targetClass = targetClass.getSuperclass();
    }
    // 當targetClass為空或者targetClass等於Object.class時會退出循環
    while (targetClass != null && targetClass != Object.class);
    // 將elements和clazz封裝成InjectionMetadata返回
    return InjectionMetadata.forElements(elements, clazz);
}

上面代碼中的 findAutowiredAnnotation() 就是在遍歷 autowiredAnnotationTypes 屬性,看字段或者方法上的註解是否存在於 autowiredAnnotationTypes 中,或者其派生註解,找到第一個就返回,不會再繼續遍歷了。

public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
		implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
    
    private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);
    
    public AutowiredAnnotationBeanPostProcessor() {
        this.autowiredAnnotationTypes.add(Autowired.class);
        this.autowiredAnnotationTypes.add(Value.class);
        try {
            this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
                                              ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
            logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
        }
        catch (ClassNotFoundException ex) {
            // JSR-330 API not available - simply skip.
        }
	}
    
}

AutowiredAnnotationBeanPostProcessor 類的構造函數中,我們可以發現 autowiredAnnotationTypes 默認添加了 @Autowired@Value 以及 @Inject (在 JSR-330 的jar包存在於當前環境時)。

至此,使用 @Autowired 修飾的字段和方法已經封裝成 InjectionMetadata 並放在 injectionMetadataCache 緩存中,便於後續使用。

AutowireAnnotationBeanPostProcessor#postProcessProperties

postProcessMergedBeanDefinition() 調用后 bean 就會進行實例化接着調用 postProcessProperties()

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 獲取緩存中的 InjectionMetadata
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 進行屬性的注入
        metadata.inject(bean, beanName, pvs);
    }
    catch (BeanCreationException ex) {
        throw ex;
    }
    catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    // 返回注入的屬性
    return pvs;
}

// InjectMetadata.java
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 獲取檢查后的元素
    Collection<InjectedElement> checkedElements = this.checkedElements;
    // 如果checkedElements不為空就使用checkedElements,否則使用injectedElements
    Collection<InjectedElement> elementsToIterate =
        (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
        // 遍歷elementsToIterate
        for (InjectedElement element : elementsToIterate) {
            if (logger.isTraceEnabled()) {
                logger.trace("Processing injected element of bean '" + beanName + "': " + element);
            }
            // AutowiredFieldElement、AutowiredMethodElement這兩個類繼承InjectionMetadata.InjectedElement,各自重寫了inject方法
            element.inject(target, beanName, pvs);
        }
    }
}

AutowiredFieldElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 強轉成Field類型
    Field field = (Field) this.member;
    Object value;
    if (this.cached) {
        // 如果緩存過,直接使用緩存的值,一般第一次注入都是false
        value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    }
    else {
        // 構建依賴描述符
        DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
        desc.setContainingClass(bean.getClass());
        Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        try {
            // 進行依賴解決,獲取符合條件的bean
            value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
        }
        catch (BeansException ex) {
            throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
        }
        // 加鎖
        synchronized (this) {
            // 如果沒有被緩存
            if (!this.cached) {
                // 找到了需要的bean || 該字段是必要的
                if (value != null || this.required) {
                    // 將依賴描述符賦值給cachedFieldValue
                    this.cachedFieldValue = desc;
                    // 註冊bean的依賴關係,用於檢測是否循環依賴
                    registerDependentBeans(beanName, autowiredBeanNames);
                    // 如果符合條件的bean只有一個
                    if (autowiredBeanNames.size() == 1) {
                        String autowiredBeanName = autowiredBeanNames.iterator().next();
                        // beanFactory含有名為autowiredBeanName的bean && 類型是匹配的
                        if (beanFactory.containsBean(autowiredBeanName) &&
                            beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                            // 將該屬性解析到的bean的信息封裝成ShortcutDependencyDescriptor
                            // 之後可以通過調用resolveShortcut()來間接調beanFactory.getBean()快速獲取bean
                            this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                desc, autowiredBeanName, field.getType());
                        }
                    }
                }
                else {
                    this.cachedFieldValue = null;
                }
                // 緩存標識設置為true
                this.cached = true;
            }
        }
    }
    // 如果找到了符合的bean,設置字段可訪問,利用反射設置值
    if (value != null) {
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}

上面代碼中的 beanFactory.resolveDependency() 在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏不再贅述;同樣 registerDependentBeans() 最終會調用 DefaultSingletonBeanRegistry.registerDependentBean() ,該方法在 Spring IoC getBean 方法詳解 一文中有介紹過,這裏也不再贅述。

AutowiredMethodElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 檢查是否需要跳過
    if (checkPropertySkipping(pvs)) {
        return;
    }
    // 強轉成Method類型
    Method method = (Method) this.member;
    Object[] arguments;
    if (this.cached) {
        // Shortcut for avoiding synchronization...
        // 如果緩存過,直接調用beanFactory.resolveDependency()返回符合的bean
        arguments = resolveCachedArguments(beanName);
    }
    else {
        // 獲取參數數量
        int argumentCount = method.getParameterCount();
        arguments = new Object[argumentCount];
        // 創建依賴描述符數組
        DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];
        // 記錄用於自動注入bean的名稱集合
        Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        // 遍歷參數
        for (int i = 0; i < arguments.length; i++) {
            // 將方法和參數的下標構建成MethodParameter,這裏面主要記錄了參數的下標和類型
            MethodParameter methodParam = new MethodParameter(method, i);
            // 將MethodParameter構建成DependencyDescriptor
            DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
            currDesc.setContainingClass(bean.getClass());
            descriptors[i] = currDesc;
            try {
                // 進行依賴解決,找到符合條件的bean
                Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                if (arg == null && !this.required) {
                    arguments = null;
                    break;
                }
                arguments[i] = arg;
            }
            catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
            }
        }
        // 這裏跟字段注入差不多,就是註冊bean的依賴關係,並且緩存每個參數的ShortcutDependencyDescriptor
        synchronized (this) {
            if (!this.cached) {
                if (arguments != null) {
                    DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);
                    registerDependentBeans(beanName, autowiredBeans);
                    if (autowiredBeans.size() == argumentCount) {
                        Iterator<String> it = autowiredBeans.iterator();
                        Class<?>[] paramTypes = method.getParameterTypes();
                        for (int i = 0; i < paramTypes.length; i++) {
                            String autowiredBeanName = it.next();
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                cachedMethodArguments[i] = new ShortcutDependencyDescriptor(descriptors[i], autowiredBeanName, paramTypes[i]);
                            }
                        }
                    }
                    this.cachedMethodArguments = cachedMethodArguments;
                }
                else {
                    this.cachedMethodArguments = null;
                }
                this.cached = true;
            }
        }
    }
    // 找到了符合條件的bean
    if (arguments != null) {
        try {
            // 設置方法可訪問,利用反射進行方法調用,傳入參數
            ReflectionUtils.makeAccessible(method);
            method.invoke(bean, arguments);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

構造器注入

構造器注入就是通過調用 determineCandidateConstructors() 來返回合適的構造器。

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) throws BeanCreationException {

    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
    // 緩存為空
    if (candidateConstructors == null) {
        // Fully synchronized resolution now...
        // 這裏相當於double check
        synchronized (this.candidateConstructorsCache) {
            candidateConstructors = this.candidateConstructorsCache.get(beanClass);
            if (candidateConstructors == null) {
                Constructor<?>[] rawCandidates;
                try {
                    // 獲取beanClass的所有構造函數
                    rawCandidates = beanClass.getDeclaredConstructors();
                }
                catch (Throwable ex) {
                    throw new BeanCreationException(beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() +"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
                }
                // 存放標註了@Autowired註解的構造器
                List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                // 存放標註了@Autowired註解,並且required為true的構造器
                Constructor<?> requiredConstructor = null;
                Constructor<?> defaultConstructor = null;
                for (Constructor<?> candidate : rawCandidates) {
                    // 獲取構造器上的@Autowired註解信息
                    MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
                    if (ann == null) {
                        // 如果沒有從候選者找到註解,則嘗試解析beanClass的原始類(針對CGLIB代理)
                        Class<?> userClass = ClassUtils.getUserClass(beanClass);
                        if (userClass != beanClass) {
                            try {
                                Constructor<?> superCtor =
userClass.getDeclaredConstructor(candidate.getParameterTypes());
                                ann = findAutowiredAnnotation(superCtor);
                            }
                            catch (NoSuchMethodException ex) {
                                // Simply proceed, no equivalent superclass constructor found...
                            }
                        }
                    }
                    if (ann != null) {
                        // 如果requiredConstructor不為空,代表有多個標註了@Autowired且required為true的構造器,此時Spring不知道選擇哪個拋出異常
                        if (requiredConstructor != null) {
                            throw new BeanCreationException(beanName, "Invalid autowire-marked constructor: " + candidate +". Found constructor with 'required' Autowired annotation already: " + requiredConstructor);
                        }
                        // 獲取@Autowired註解的reuired屬性的值
                        boolean required = determineRequiredStatus(ann);
                        if (required) {
                            // 如果當前候選者是@Autowired(required = true),則之前不能存在其他使用@Autowire註解的構造函數,否則拋異常
                            if (!candidates.isEmpty()) {
                                throw new BeanCreationException(beanName,"Invalid autowire-marked constructors: " + candidates +". Found constructor with 'required' Autowired annotation: " + candidate);
                            }
                            // required為true將當前構造器賦值給requiredConstructor
                            requiredConstructor = candidate;
                        }
                        // 將當前構造器加入進候選構造器中
                        candidates.add(candidate);
                    }
                    // 沒有標註了@Autowired註解且參數長度為0,賦值為默認構造器
                    else if (candidate.getParameterCount() == 0) {
                        defaultConstructor = candidate;
                    }
                }
                // 有標註了@Autowired註解的構造器
                if (!candidates.isEmpty()) {
                    // Add default constructor to list of optional constructors, as fallback.
                    // 沒有標註了@Autowired且required為true的構造器
                    if (requiredConstructor == null) {
                        // 默認構造器不為空
                        if (defaultConstructor != null) {
                            // 將默認構造器加入進候選構造器中
                            candidates.add(defaultConstructor);
                        }
                    }
                    // 將候選者賦值給candidateConstructors
                    candidateConstructors = candidates.toArray(new Constructor<?>[0]);
                }
                // 只有1個構造器 && 參數長度大於0(非默認構造器),只能用它當做候選者了
                else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
                    candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
                }
                // 只有1個構造器 && 參數長度大於0,只能用它當做候選者了
                else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
                         defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor};
                }
                else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor};
                }
                // 返回一個空的Constructor
                else {
                    candidateConstructors = new Constructor<?>[0];
                }
                // 緩存候選的構造器
                this.candidateConstructorsCache.put(beanClass, candidateConstructors);
            }
        }
    }
    // 如果候選構造器長度大於0,直接返回,否則返回null
    return (candidateConstructors.length > 0 ? candidateConstructors : null);
}

關於 SmartInstantiationAwareBeanPostProcessor 接口的調用時機,在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏就不再贅述了。

總結

本文主要介紹了 Spring 對 @Autowired 註解的主要處理過程,結合前面的 Spring IoC getBean 方法詳解 和 Spring IoC createBean 方法詳解 文章一起看才能更好的理解。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

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

【其他文章推薦】

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

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

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

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

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

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

可能不到10萬就買到1.5T自動7座SUV,你還要考慮銳界?

內飾設計感更強中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

前言

作為長安首款7座SUV,CX70上市已經有大半年的時間了,實惠的價格能買到大7座SUV,寬敞的空間布局也比較符合現在家庭用戶的需求,但是其比較單一的1.6L+5MT的動力總成,對那些不會開手動擋或者對動力有追求的消費者來說,就比較糾結了,最終1.5T+6AT的解決方案出來了,能讓你滿意嗎?

外觀

更加霸氣

前臉的進氣格柵變化是比較明顯的,大小不一的鍍鉻飾塊錯落有致的分佈在中網上,看上去精緻了許多,門把手和側護板上增加了鍍鉻裝飾,提升了不少檔次感,外擴的后保險杠造型讓車身長度增加了25mm,並且覆蓋了金屬裝飾板,既起到保護作用,又使硬朗的造型中透露着細膩感。

內飾

設計感更強

中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

空間

一如既往的大

2780mm的軸距,加上調整后的保險杠樣式使其車身增加了25mm至4705mm,在同級別競爭對手來說有一定的優勢,採用2+3+2的座椅布局,雙色拼接的座椅設計視覺效果出色。

動力

新的組合

動力方面新增了一款1.5T發動機,最大功率為150馬力,在同排量車型中來說表現中規中矩,與之匹配的是6擋手動,或者是來自愛信的6擋手自一體變速箱,想購買該款車型的消費者終於等來了渦輪增壓車型。

總結:從產品力來看,長安CX70終於補齊了沒有自動擋車型的短板,在逐漸激烈的7座SUV市場里提升了不少的戰鬥力,但具體性能表現如何,還得上市后才見分曉,還有價格上小編推測應該是很有吸引力的一個價格。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

日本疫情下曝核電廠群聚問題 作業人員無可避免的「三密」

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

全新蛻變 試駕東風風行景逸X5

0L發動機的車型只提供手動擋版本,值得一贊的是,全新一代景逸X5繼續延續了景逸家族加大號基因的優良傳統,軸距達到了2720mm,讓該車在同級別中擁有了十分可觀的使用空間和乘坐空間。在駕駛過程中,每個檔位的清晰,雖無吸入感,但各擋位之間的行程較短,掛檔很順手。

如今SUV車型在汽車市場上的火爆程度,相信大家也都有目共睹,自主品牌對合資SUV造成了很大的衝擊,很多人覺得自主汽車熱賣是因為便宜,這個理由當然是有的,但是它僅僅只是其中之一。事實上最近幾年自主品牌的內飾設計、動力匹配、整體品質、科技配置及整體工藝都有了巨大的提升,穩定的質量和口碑才是自主品牌SUV得以持續熱銷的核心。

放眼國內SUV市場,由東風汽車話旗下的景逸X3、景逸X5、景逸XV組成的景逸SUV陣形,攻打市場,各車型分別從不同層面滿足消費者需求,在SUV市場有着屬於自己的一片領地。

新景逸x5的上市,其7.99萬-10.89萬的價格更是显示出東風汽車十足的把握和搶佔SUV市場的決心。

景逸X5是東風風行推出的一款緊湊型SUV,第一代車型於2013年8月30日正式上市,但由於造型、和口碑原因,市場表現一直不溫不火,12月17號全新的東風風行景逸X5上市,與上一代產品相比,全新一代景逸X5在產品力上有了不小的提升,最為直觀的感受便是在外觀設計上,東風柳汽好像打通了“任督二脈”知道什麼樣的外觀能贏的市場。

全新景逸X5搭載1.6L和2.0L兩款自然吸氣發動機,1.6L發動機除匹配手動擋變速箱外,還提供匹配CVT變速箱的車型供消費者選擇,而搭載2.0L發動機的車型只提供手動擋版本,值得一贊的是,全新一代景逸X5繼續延續了景逸家族加大號基因的優良傳統,軸距達到了2720mm,讓該車在同級別中擁有了十分可觀的使用空間和乘坐空間。

在駕駛過程中,每個檔位的清晰,雖無吸入感,但各擋位之間的行程較短,掛檔很順手。全新景逸X5的離合器的結合點相對較寬且很容易找到,一般不會出現熄火的情況。

值得一提的是,新景逸X5還配備了自動駐車功能,在2.0L的車型都有此功能,對於手動檔操作還不是很熟練的人來說,這個功能很實用,特別是擁堵的斜坡上,大大降低操作難度。

全新景逸X5在內飾上的變化是最明顯的,採用了全新的設計風格,整個設計是符合當下整個設計潮流,而且採用T型橫向設計,在儀錶台主體上採用搪塑發泡工藝,特別是將主流的懸浮式显示屏及黑棕雙色內飾運用其中,顯得非常年輕運動感。

其實給我的感覺內飾最大的亮點還屬於中控上的8英寸懸浮式显示屏,智能互聯繫統不僅集車載導航、人機互聯、語音識別、藍牙等影音娛功能於一體,還可以實現與智能手機的完美連接,確實讓整體氛圍上,顯的更加高端了,賣一台這樣價位然後有B級車的科技體驗,這是非常吸引人的。

記得,在廣州車展上,東風柳汽車副總經理姚利文接受專訪時說,全新景逸X5這個價格很親民、極具誠意,這在十萬元級精品SUV市場里是非常具有競爭力的。而且它擁有‘8年或16萬公里’超長保修政策,在業內也很罕見,這是對新景逸X5品質的充分信心。

然後明年下半年全新景逸X5還會陸續推出1.5T和1.3T版,產品序列進一步豐富,我相信,在整個景逸家族的陣形中,景逸X5肯定是擔當著最重要的“開山劈嶺”角色,景逸X5也承載了東風汽車的造車最高水準,我們期待這個“大哥”景逸X5如何帶領眾兄弟,開闢屬於自己的領地。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

野火增長威脅物種 專家警告:亞馬遜雨林恐在20年內崩潰

摘錄自2020年8月8日自由時報報導

根據今(8)日公布的最新數據,7月巴西亞馬遜雨林的野火數量相較去年同期增加28%,專家警告,亞馬遜恐在20年內崩潰,並對當地土著、野生動植物、健康和氣候造成災難性影響。

綜合外媒報導,巴西國家太空研究院(INPE)統計,亞馬遜地區7月共出現6803起野火,儘管這項數據未達到去年高峰值,但根據巴西綠色和平組織分析顯示,7月30日單日就出現超過千起野火,是自2005年以來7月單日最多的野火紀錄。

美國太空總署(NASA)也在月初表示,北大西洋熱帶地區的地表溫度升高,導致亞馬遜河南部變得乾燥易燃,傳統「焚田」可能導致失控的大火。

環保主義人士則指責巴西總統波索納洛(Jair Bolsonaro)自去年起將亞馬遜地區大量土地改為農業或工業用地,導致亞馬遜大火情勢惡化。

生物多樣性
土地利用
國際新聞
巴西
亞馬遜雨林
野火

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

這三款車如果高配當中配來賣 可能都是神車了

5LV6發動機布局,入門售價比西瑪低,如果讓你選。你會。其實西瑪綜合能力並不遜色同級別對手,豐富配置與乘坐舒適度都是加分項目,如果在售價上有所讓步,編者相信其銷量必然不亞於同級別對手。標緻4008指導價:18。57-27。

前言

不少汽車廠商推出每一款新車型,上市前都會把其炒得沸沸揚揚,吸引更多消費者關注,這也是廠商慣用的營銷手法。然而有些車型由於價格過高,所以上市后銷量比較淡,編者覺得,如果下面幾款車,降價兩萬一定會讓消費者更加心動。

本田-冠道

指導價:26.98-32.98萬

冠道無論是在外觀內飾都營造出豪華氣氛,空間以及動力都是在同級別中數一數二,冠道算是本田旗艦SUV稱號,而且也具有這實力,但四驅車型售價將近30萬,而相比同級別對手漢蘭達四驅車型售價也只是25萬左右。

冠道上市之初,其空間以及動力都是其一大亮點,其實在實用性方面比七座車型更實用,而且在乘坐舒適度上都比七座車型上一個檔次,之所以銷量上沒取得成功,這也源自於市場競爭力越越來大,在這一細分市場里,冠道還是需要調整價格,或者新增車型,降低入門售價。

有消息稱明年冠道將會推出1.5T車型,降低入門售價以及產品布局更廣,而2.0T車型才是精髓所在,無奈售價過高,但如果降價兩萬還是非常值得購買。

日產-西瑪

指導價:23.48-26.78萬

西瑪定位偏向運動,外觀與內飾都具備運動風格,但實際駕馭起來的運動感卻不是那麼強烈,其實西瑪與天籟底子特性相差不大,西瑪能兼顧舒適還具備一定運動感,但由於天籟起售價遠遠低於西瑪,都是同品牌這讓西瑪銷量自然不會比天籟高。

運動B級車,可以說這消費群體更多是來自年輕人,而他們對於空間有一定需求,滿足日常出行,動力性能也是這消費群體更注重,銳志相比西瑪更有誘惑力,2.5LV6發動機布局,入門售價比西瑪低,如果讓你選?你會…

其實西瑪綜合能力並不遜色同級別對手,豐富配置與乘坐舒適度都是加分項目,如果在售價上有所讓步,編者相信其銷量必然不亞於同級別對手。

標緻4008

指導價:18.57-27.37萬

標緻4008上市之初,無論是內飾以及外觀都受到高度讚賞,4008也可以說是賣設計的車型,特別是其內飾設計,營造出科技與戰鬥氣息,視覺感官衝擊可以說是讓人眼前一亮!

上月廣州車展,編者也特地感受一番,乘坐舒適度以及內飾氛圍,絕對不止是華而不實。但標緻4008對於18.57萬的起售價與同級別對手本田CR-V、福特翼虎、大眾途觀可以說是競爭力不強。

對於消費者來說,標緻這品牌或許其售價在15萬左右能接受,4008也是標緻一次新的嘗試,但其內飾氛圍確實非常吸引人、檔次感特強。

全文總結

對於以上車型,其實在綜合表現能力並不遜色,只是售價方面稍高,但對於資金充裕消費者還是值得購買,當然啦,有優惠那就更好了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

20萬左右就能買到V6+后驅?看看車主們怎麼說?

銳志內飾做工,用料非常講究,採用了大面積軟質材料,在視覺感受以及觸感都有不錯的質感,雖然內飾顯得有些老氣,但還是很顯檔次。目前行駛里程:目前行駛36800公里,綜合油耗在11。5L/100km,銳志依然還是一款讓人省心的車型。

對於性能控來說,只有面對大排量自吸,才毫無抵抗力,然而想要買6缸機或許會覺得要40萬左右,但也有性價比之選,只要20萬左右你就能擁有6缸自吸發動機,也許你們已經想到了!沒錯,就是銳志,我們今天就來看看已經購買該車的車主,如何評價這款車!

豐田-銳志

指導價:20.98-31.48萬

車主一:神秘人

購車車型:銳志 2013款 2.5S 菁銳版

裸車價格:17.90萬

車主點評:毋庸置疑,最滿意莫非是動力輸出的平順性以及V6后驅。行駛在鋪裝路面都能非常寫意地享受駕駛樂趣,還有同級別較少的前置后驅,只要捨得給油,都有非常不錯的直觀感受,轉速在5000轉過後,發動機聲以及排氣聲浪非常渾厚,特別吸引人!畢竟,誰沒有一顆年輕的心呢?

目前行駛里程:銳志目前行駛了26000公里,綜合油耗在10.7L/100km,油耗表現,很滿意,畢竟是一台V6發動機。

車主二:孤獨的影子

購買車型:銳志 2013款 2.5V 尚銳版

裸車價格:21.48萬

車主點評:雖然是2013款車型,但外觀依然時尚,以現在看來一點也不過時,X前臉非常霸氣,車身也顯得修長優雅,鐮刀式尾燈辨識度非常高。銳志內飾做工,用料非常講究,採用了大面積軟質材料,在視覺感受以及觸感都有不錯的質感,雖然內飾顯得有些老氣,但還是很顯檔次。

目前行駛里程:目前行駛36800公里,綜合油耗在11.5L/100km,銳志依然還是一款讓人省心的車型。

車主三:因為有本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

澳洲大學海洋實驗:高溫打亂食物金字塔 頂層掠食者潰散

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案