電動汽車分時租賃入駐上海 2017年底前投放6000輛

國網上海市電力公司2月3日與上海國際汽車城(集團)有限公司簽署戰略合作協定。今後,雙方將在現有國網充電樁點位基礎上再增加EVCARD共用車位;同時在充電業務領域開展合作,積極共建充電樁和共用充電資源,從而推動申城新能源車分時租賃點的有效擴容,實現城市公共資源的合理利用。

作為合作計畫的首批現實成果,當天,三個合作網站正式啟用。

EVCARD是中國首個電動汽車分時租賃品牌。它于2013年進行開發,並於2015年1月在上海嘉定率先投入市場。截至2016年1月份,EVCARD在上海的用戶數達到38940人,訂單數量也達到25765個。

自從有了電動車分時租賃,同濟大學嘉定校區的學生王蓓出門時多了一重選擇,她來到附近的“大學裡社區”EVCARD電動汽車租賃網點,熟練地刷卡取車,隨後便開著這輛租來的榮威E50純電動車上路。辦完事,她開車返回取車網點,停好車輛,刷卡還車。借車總耗時107分鐘,每分鐘0.5元人民幣,計時收費,共花費53.5元,這甚至比她出門坐計程車還要便宜幾十元。

業內人士測算指出,目前電動車租賃行業之所以不盈利,首先在於網點不夠,取還車、充電都不太方便,當下的任務是結合城市交通需求來進行新增網點規劃;另一個原因在於電動汽車價格太高,這就導致了租車的價格下不來,解決這個問題,要靠技術發展,也需要時間。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

南投搬家前需注意的眉眉角角,別等搬了再說!

FreeSql v0.11 幾個實用功能說明

FreeSql 開源發布快一年了,立志成為 .Net 平台方便好用的 ORM,倉庫地址:

隨着不斷的迭代更新,越來越穩定,也越來越強大。預計在一周年的時候(2020年1月1日)發布 1.0 正式版本。

金九銀十的日子過去了,在這個銅一般的月份里,鄙人做了幾個重大功能,希望對使用者開發提供更大的便利。

  • 一、Dto 映射查詢
  • 二、IncludeMany 聯級加載
  • 三、Where(a => true) 邏輯表達式解析優化
  • 四、SaveManyToMany 聯級保存多對多集合屬性
  • 五、遷移實體 – 到指定表名
  • 六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert
  • 七、ISelect.ToDelete 高級刪除
  • 八、全局過濾器

以下的代碼,先決定義代碼如下 :

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=|DataDirectory|\db1.db;Max Pool Size=10";)
    .UseAutoSyncStructure(true) //自動同步實體結構到數據庫
    .Build();

public class Blog
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

一、Dto 映射查詢

class Dto
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int xxx { get; set; }
}

fsql.Select<Blog>().ToList<Dto>();
//SELECT Id, Url FROM Blog

fsql.Select<Blog>().ToList(a => new Dto { xxx = a.Rating} );
//SELECT Id, Url, Rating as xxx FROM Blog
//這樣寫,附加所有映射,再額外映射 xxx

fsql.Select<Blog>().ToList(a => new Blog { Id = a.Id }) 
//這樣寫,只查詢 id

fsql.Select<Blog>().ToList(a => new { a.Id }) 
//這樣寫,只查詢 id,返回匿名對象

映射支持單表/多表,是在查詢數據之前映射(不是先查詢所有字段再到內存映射)

查找規則,查找屬性名,會循環內部對象 _tables(join 查詢後會增長),以 主表優先查,直到查到相同的字段。

如:

A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射。也可以指定 id = C.id 映射。

友情提醒:在 dto 可以直接映射一個導航屬性

二、IncludeMany 聯級加載

之前已經實現,有設置關係,和未設置關係 的導航集合屬性聯級加載。

有設置關係的(支持一對多、多對多):

fsql.Select<Tag>().IncludeMany(a => a.Goods).ToList();

未設置關係的,臨時指定關係(只支持一對多):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Where(b => b.TagId == a.Id));

只查詢每項子集合的前幾條數據,避免像EfCore加載所有數據導致IO性能低下(比如某商品下有2000條評論):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Take(10));

上面已有的 IncludeMany 功能還不夠自由靈活。

新功能1:在 Dto 上做映射 IncludeMany

老的 IncludeMany 限制只能在 ISelect 內使用,必須要先查上級數據,解決這個問題我們做了直接在 Dto 上做映射:

查詢 Goods 商品表,分類1、分類2、分類3 各10條數據

//定義臨時類,也可以是 Dto 類
class Dto {
    public int TypeId { get; set; }
    public List<Goods > GoodsList { get; set; }
}

var dto = new [] { 1,2,3 }.Select(a => new Dto { TypeId = a }).ToList();
dto.IncludeMany(d => d.GoodsList.Take(10).Where(gd => gd.TypeId == d.TypeId));

//執行后,dto 每個元素.Vods 將只有 10條記錄

現在 IncludeMany 不再是 ISelect 的專利,普通的 List<T> 也可以用它來貪婪加載數據,並準確填充到內部各元素中。

新功能2:查詢子集合表的指定字段

老的 IncludeMany 限制只能查子表的所有字段,子表過段多過的話比較浪費 IO 性能。

新功能可以設置子集合返回部分字段,避免子集合字段過多的問題。

fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods { Id = b.Id, Title = b.Title }));
//只查詢 goods 表 id, title 字段,再作填充

三、Where(a => true) 邏輯表達式解析優化

相信很多 ORM 解析表達式的時候處理不了這個問題,我們之前已經解決了 99%。

這個月發現還有一餘孽未清,發現問題后及時解決了,並增加單元測試代碼以絕後患。

四、SaveManyToMany 聯級保存多對多集合屬性

在此之前,FreeSql.DbContext 和 倉儲實現,已經實現了聯級保存功能,如下:

聯級保存功能可實現保存對象的時候,將其【OneToMany】、【ManyToMany】導航屬性集合也一併保存。

全局關閉:

fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);

局部關閉:

var repo = fsql.GetRepository<T>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;

新功能:

保存實體的指定【多對多】導航屬性,SaveManyToMany 方法實現在 BaseRepository、DbContext。

解決問題:當實體類導航數據過於複雜的時候,選擇關閉聯級保存的功能是明智之選,但是此時【多對多】數據保存功能寫起來非常繁瑣麻煩(因為要與現有數據對比后保存)。

var song = new Song { Id = 1 };
song.Tags = new List<Tag>();
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
repo.SaveManyToMany(song, "Tags");
//輕鬆保存 song 與 tag 表的關聯

機制規則與聯級保存的【多對多】一樣,如下:

我們對中間表的保存是完整對比操作,對外部實體的操作只作新增(注意不會更新)

  • 屬性集合為空時,刪除他們的所有關聯數據(中間表)
  • 屬性集合不為空時,與數據庫存在的關聯數據(中間表)完全對比,計算出應該刪除和添加的記錄

五、遷移實體 – 到指定表名

fsql.CodeFirst.SyncStructure(typeof(Log), "Log_1"); //遷移到 Log_1 表
fsql.CodeFirst.SyncStructure(typeof(Log), "Log_2"); //遷移到 Log_2 表

在此功能上,我們對分表功能做了點升級,以下動作都會做遷移動作:

fsql.Select<Log>().AsTable((_, oldname) => $"{oldname}_1");
fsql.GetRepository<Log>(null, oldname => $"{oldname}_1");

六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert

FreeSql 提供了多種插入或更新方法,v0.11 之前主要使用 FreeSql.Repository/FreeSql.DbContext 庫提供的方法實現。

FreeSql.Repository 之 InsertOrUpdate

此方法與 FreeSql.DbContext AddOrUpdate 方法功能一樣。

var repo = fsql.GetRepository<T>();
repo.InsertOrUpdate(實體);

如果內部的狀態管理存在數據,則更新。

如果內部的狀態管理不存在數據,同查詢數據庫,是否存在。

存在則更新,不存在則插入

缺點:不支持批量操作

新功能:MySql 特有功能 On Duplicate Key Update

FreeSql.Provider.MySql 和 FreeSql.Provider.MySqlConnector 在 v0.11.11 版本已支持 MySql 特有的功能,On Duplicate Key Update。

這個功能也可以實現插入或更新數據,並且支持批量操作。

class TestOnDuplicateKeyUpdateInfo
{
    [Column(IsIdentity = true)]
    public int id { get; set; }
    public string title { get; set; }
    public DateTime time { get; set; }
}

var item = new TestOnDuplicateKeyUpdateInfo { id = 100, title = "title-100", time = DateTime.Parse("2000-01-01") };
fsql.Insert(item)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`, `time`) VALUES(100, 'title-100', '2000-01-01 00:00:00.000')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = VALUES(`time`)

OnDuplicateKeyUpdate() 之後可以調用的方法:

方法名 描述
IgnoreColumns 忽略更新的列,機制和 IUpdate.IgnoreColumns 一樣
UpdateColumns 指定更新的列,機制和 IUpdate.UpdateColumns 一樣
Set 手工指定更新的列,與 IUpdate.Set 功能一樣
SetRaw 作為 Set 方法的補充,可傳入 SQL 字符串
ToSql 返回即將執行的 SQL 語句
ExecuteAffrows 執行,返回影響的行數

IInsert 與 OnDuplicateKeyUpdate 都有 IgnoreColumns、UpdateColumns 方法。

當插入實體/集合實體的時候,忽略了 time 列,代碼如下:

fsql.Insert(item)
    .IgnoreColumns(a => a.time)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`) VALUES(200, 'title-200')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = '2000-01-01 00:00:00.000'

我們發現,UPDATE time 部分變成了常量,而不是 VALUES(`time`),機制如下:

當 insert 部分中存在的列,在 update 中將以 VALUES(`字段`) 的形式設置;

當 insert 部分中不存在的列,在 update 中將為常量形式設置,當操作實體數組的時候,此常量為 case when … end 執行(與 IUpdate 一樣);

新功能2:PostgreSQL 特有功能 On Conflict Do Update

使用方法 MySql OnDuplicateKeyUpdate 大致相同。

七、ISelect.ToDelete 高級刪除

默認 IDelete 不支持導航對象,多表關聯等。ISelect.ToDelete 可將查詢轉為刪除對象,以便支持導航對象或其他查詢功能刪除數據,如下:

fsql.Select<T1>().Where(a => a.Options.xxx == 1).ToDelete().ExecuteAffrows();

注意:此方法不是將數據查詢到內存循環刪除,上面的代碼產生如下 SQL 執行:

DELETE FROM `T1` WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)

複雜刪除使用該方案的好處:

  • 刪除前可預覽測試數據,防止錯誤刪除操作;
  • 支持更加複雜的刪除操作(IDelete 默認只支持簡單的操作),甚至在 ISelect 上使用 Limit(10) 將只刪除附合條件的前 10條記錄;

還有 ISelect.ToUpdate 高級更新數據功能,使用方法類似

八、全局過濾器

FreeSql 基礎層實現了 Select/Update/Delete 可設置的全局過濾器功能。

public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();

fsql.GlobalFilter
    .Apply<TestAddEnum>("test1", a => a.Id == TenantId.Value)
    .Apply<AuthorTest>("test2", a => a.Id == 111)
    .Apply<AuthorTest>("test3", a => a.Name == "11");

Apply 泛型參數可以設置為任何類型,當使用 Select/Update/Delete 方法時會進行過濾器匹配嘗試(try catch):

  • 匹配成功的,將附加 where 條件;
  • 匹配失敗的,標記下次不再匹配,避免性能損耗;

如何禁用?

fsql.Select<TestAddEnum>().ToList(); //所有生效
fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); //禁用 test1
fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); //禁用所有

fsql.Update/Delete 方法效果同上。

注意:IFreeSql.GlobalFilter 與 倉儲過濾器 不是一個功能,可以同時生效

鳴謝

感謝反饋 bug 的朋友!

倉庫地址:

請移步更新日誌:https://github.com/2881099/FreeSql/wiki/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

如何構建自己的 react hooks

我們組的前端妹子在組內分享時談到了 react 的鈎子,趁此機會我也對我所理解的內容進行下總結,方便更多的同學了解。在 React 的 v16.8.0 版本里添加了 hooks 的這種新的 API,我們非常有必要了解下他的使用方法,並能夠結合我們的業務編寫幾個自定義的 hooks。

1. 常用的一個 hooks

官方中提供了幾個內置的鈎子,我們簡單了解下他們的用法。

1.1 useState: 狀態鈎子

需要更新頁面狀態的數據,我們可以把他放到 useState 的鈎子里。例如點擊按鈕一下,數據加 1 的操作:

const [count, setCount] = useState(0);

return (<>
    <p>{ count}</p>
    <button onClick = {
        () => setCount(count + 1)
    }> add 1 </button>
    </>
);

在 typescript 的體系中,count 的類型,默認就是當前初始值的類型,例如上面例子中的變量就是 number 類型。如果我們想自定義這個變量的類型,可以在 useState 後面進行定義:

const [count, setCount] = useState<number | null>(null); // 變量count為number類型或者null類型

同時,使用 useState 改變狀態時,是整個把 state 替換掉的,因此,若狀態變量是個 object 類型的數據,我只想修改其中的某個字段,在之前 class 組件內調用 setState 時,他內部會自動合併數據。

class Home extends React.Component {
    state = {
        name: 'wenzi',
        age: 20,
        score: 89
    };

    update() {
        this.setState({
            score: 98
        }); // 內部自動合併
    }
}

但在 function 組件內使用 useState 時,需要自己先合併數據,然後再調用方法,否則會造成字段的丟失。

const [person, setPerson] = useState({
    name: 'wenzi',
    age: 20,
    score: 89
});

setPerson({
    ...person,
    {
        score: 98
    }
}); // 先合併數據 { name: 'wenzi', age: 20, score: 98 }
setPerson({
    score: 98
}); // 僅傳入要修改的字段,后name和age字段丟失

1.2 useEffect: 副作用鈎子

useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合。

useEffect 鈎子在組件初始化完畢時,一定會執行一次,在組件重新渲染的過程中,是否還要 update,還要看傳入的第 2 個參數。

  1. 當只有回調函數這一個參數時,組件的每次更新,回調都會執行;
  2. 當有 2 個參數時,只有第 2 參數里的數據發生變化時,回調才執行;
  3. 只想在組件初始化完畢時只執行一次,第 2 個參數可以傳入一個空的數組;

我們可以看下這個例子,無論點擊 add按鈕 還是 settime按鈕 ,useEffect 的回調都會執行:

const Home = () => {
    const [count, setCount] = useState(0);
    const [nowtime, setNowtime] = useState(0);

    useEffect(() => {
        console.log('count', count);
        console.log('nowtime', nowtime);
    });

    return ( <>
        <p>count: {count} </p>
        <p>nowtime: {nowtime} </p>
        <button onClick = {() => setCount(count + 1)}> add 1 </button>
        <button onClick = {() => setNowtime(Date.now())} > set now time </button>
    </>);
};

若改成下面的這樣,回調僅會在 count 發生變化時才會在控制台輸出,僅修改 nowtime 的值時沒有輸出:

useEffect(() => {
    console.log('count', count);
    console.log('nowtime', nowtime);
}, [count]);

useEffect 的回調函數還可以返回一個函數,這個函數在 effect 生命周期結束之前調用。為防止內存泄漏,清除函數會在組件卸載前執行。另外,如果組件多次渲染,則在執行下一個 effect 之前,上一個 effect 就已被清除

基於上面的代碼,我們稍微修改一下:

useEffect(() => {
    console.log('count', count);
    console.log('nowtime', nowtime);

    return () => console.log('effect callback will be cleared');
}, [count]);

基於這個機制,在一些存在添加綁定和取消綁定的案例上特別合適,例如監聽頁面的窗口大小變化、設置定時器、與後端的 websocket 接口建立連接和斷開連接等,都可以預計 useEffect 進行二次的封裝,形成自定義的 hook。關於自定義 hook,下面我們會講到。

1.3 useMemo 和 useCallback

function 組件中定義的變量和方法,在組件重新渲染時,都會重新重新進行計算,例如下面的這個例子:

const Home = () => {
    const [count, setCount] = useState(0);
    const [nowtime, setNowtime] = useState(0);

    const getSum = () => {
        const sum = ((1 + count) * count) / 2;
        return sum + ' , ' + Math.random(); // 這個random是為了看到區別
    };

    return ( <>
        <p> count: {count}< /p>
        <p> sum: {getSum()}</p>
        <p> nowtime: {nowtime}</p>
        <button onClick = {() => setCount(count + 1)} > add 1 </button>
        <button onClick = {() => setNowtime(Date.now())}> set now time </button>
    </>);
};

這裡有 2 個按鈕,一個是 count+1,一個設置當前的時間戳, getSun() 方法是計算從 1 到 count 的和,我們每次點擊 add 按鈕后,sum 方法都會重新計算和。可是當我們點擊 settime 按鈕時,getSum 方法也會重新計算,這是沒有必要的。

這裏我們可以使用 useMemo 來修改下:

const sum = useMemo(() => ((1 + count) * count) / 2 + ' , ' + Math.random(), [count]);

<p> {sum} </p>;

修改后就可以看到,sum 的值只有在 count 發生變化的時候才重新計算,當點擊 settime 按鈕的時候,sum 並沒有重新計算。這要得益於 useMemo 鈎子的特性:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 返回回調里 return 的值,而且 memoizedValue 它僅會在某個依賴項改變時才重新計算。這種優化有助於避免在每次渲染時都進行高開銷的計算。如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。

在上面的例子里,只有 count 變量發生變化時,才重新計算 sum,否則 sum 的值保持不變。

useCallback 與 useMemo 類型,只不過 useCallback 返回的是一個函數,例如:

const fn = useCallback(() => {
    return ((1 + count) * count) / 2 + ' , ' + nowtime;
}, [count]);

2. 實現幾個自定義的 hook

在官方文檔里,實現了好友的在線與離線功能。這裏我們自己也學着實現幾個 hook。

2.1 獲取窗口變化的寬高

我們通過監聽resize事件來獲取實時獲取window窗口的寬高,對這個方法進行封裝后可以在生命周期結束前能自動解綁resize事件:

const useWinResize = () => {
    const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    });
    const resize = useCallback(() => {
        setSize({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })
    }, [])
    useEffect(() => {
        window.addEventListener('resize', resize);
        return () => window.removeEventListener('resize', resize);
    }, []);
    return size;
}

使用起來也非常方便:

const Home = () => {
    const {width, height} = useWinResize();

    return <div>
        <p>width: {width}</p>
        <p>height: {height}</p>
    </div>;
};

點擊鏈接可以查看demo演示。

2.2 定時器 useInterval

在前端中使用定時器時,通常要在組件生命周期結束前清除定時器,如果定時器的周期發生變化了,還要先清除定時器再重新按照新的周期來啟動。這種最常用的場景就是九宮格抽獎,用戶點擊開始抽獎后,先緩慢啟動,然後逐漸變快,接口返回中獎結果后,再開始減速,最後停止。

我們很容易想到用 useEffect 來實現這樣的一個 hook:

const useInterval = (callback, delay) => {
    useEffect(() => {
        if (delay !== null) {
            let id = setInterval(callback, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
};

我們把這段代碼用到項目中試試:

const Home = () => {
    const [count, setCount] = useState(0);

    useInterval(() => {
        console.log(count);
        setCount(count + 1);
    }, 500);

    return <div > {
        count
    } < /div>;
};

可是這段運行后很奇怪,頁面從 0 到 1 后,就再也不變了, console.log(count) 的輸出表明代碼並沒有卡死,那麼問題出在哪兒了?

React 組件中的 props 和 state 是可以改變的, React 會重渲染它們且「丟棄」任何關於上一次渲染的結果,它們之間不再有相關性。

useEffect() Hook 也「丟棄」上一次渲染結果,它會清除上一次 effect 再建立下一個 effect,下一個 effect 鎖住新的 props 和 state,這也是我們第一次嘗試簡單示例可以正確工作的原因。

但 setInterval 不會「丟棄」。 它會一直引用老的 props 和 state 直到你把它換掉 —— 不重置時間你是無法做到的。

這裏就要用到這個 hook 了,我們把 callback 存儲到 ref 中,當 callback 更新時去更新 ref.current 的值:

const useInterval = (callback, delay) => {
    const saveCallback = useRef();

    useEffect(() => {
        // 每次渲染后,保存新的回調到我們的 ref 里
        saveCallback.current = callback;
    });

    useEffect(() => {
        function tick() {
            saveCallback.current();
        }
        if (delay !== null) {
            let id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
};

當我們使用新的 useInterval 時,發現就可以自增了,點擊查看樣例。

這裏我們使用一個變量來控制增加的速度:

const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500);

useInterval(() => {
    setCount(count + 1);
}, diff);

return ( <div>
    <p> count: {count} </p>
    <p> diff: {diff}ms </p> 
    <p>
        <button onClick = {() => setDiff(diff - 50)}> 加快50ms </button> 
        <button onClick = {() => setDiff(diff + 50)} > 減慢50ms </button>
    </p>
</div>);

分別點擊兩個按鈕,可以調整count增加的速度。

3. 總結

使用react hook可以做很多有意思的事情,這裏我們也僅僅是舉幾個簡單的例子,後續我們也會更加深入了解hook的原理。

▼我是來騰訊的小小前端開發工程師,
長按識別二維碼關注,與大家共同學習、討論▼

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

南投搬家前需注意的眉眉角角,別等搬了再說!

go中的數據結構-接口interface

1. 接口的基本使用

  golang中的interface本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那麼該類就實現了該接口。與其他語言不同,golang並不需要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。

聲明

 1 type 接口名 interface{
 2     方法1
 3     方法2
 4     ...
 5    方法n 
 6 }
 7 type 接口名 interface {
 8     已聲明接口名1
 9     ...
10     已聲明接口名n
11 }
12 type iface interface{
13     tab *itab
14     data unsafe.Pointer
15 }

  接口自身也是一種結構類型,只是編譯器對其做了很多限制:

  • 不能有字段
  • 不能定義自己的方法
  • 只能聲明方法,不能實現
  • 可嵌入其他接口類型
 1 package main
 2 
 3     import (
 4         "fmt"
 5     )
 6 
 7     // 定義一個接口
 8     type People interface {
 9         ReturnName() string
10     }
11 
12     // 定義一個結構體
13     type Student struct {
14         Name string
15     }
16 
17     // 定義結構體的一個方法。
18     // 突然發現這個方法同接口People的所有方法(就一個),此時可直接認為結構體Student實現了接口People
19     func (s Student) ReturnName() string {
20         return s.Name
21     }
22 
23     func main() {
24         cbs := Student{Name:"小明"}
25 
26         var a People
27         // 因為Students實現了接口所以直接賦值沒問題
28         // 如果沒實現會報錯:cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method)
29         a = cbs       
30         name := a.ReturnName() 
31         fmt.Println(name) // 輸出"小明"
32     }

  如果一個接口不包含任何方法,那麼它就是一個空接口(empty interface),所有類型都符合empty interface的定義,因此任何類型都能轉換成empty interface。

  接口的值簡單來說,是由兩部分組成的,就是類型和數據,判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都為nil才代表接口是nil。

1 var a interface{} 
2 var b interface{} = (*int)(nil)
3 fmt.Println(a == nil, b == nil) //true false

2. 接口嵌套

  像匿名字段那樣嵌入其他接口。目標類型方法集中必須擁有包含嵌入接口方法在內的全部方法才算實現了該接口。嵌入其他接口類型相當於將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。

 1 type stringer interfaceP{
 2      string() string
 3 }
 4 
 5 type tester interface {
 6     stringer
 7     test()
 8 }    
 9 
10 type data struct{}
11 
12 func (*data) test() {}
13 
14 func (data) string () string {
15     return ""
16 }
17 
18 func main() {
19     var d data 
20     var t tester = &d 
21     t.test()
22     println(t.string())
23 }

  超集接口變量可隱式轉換為子集,反過來不行。

3. 接口的實現

golang的接口檢測既有靜態部分,也有動態部分。

  • 靜態部分
    對於具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,後續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址即可。具體實現可以看golang的提交日誌CL 20901、CL 20902。
    對於interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,並生成值

  • 動態部分
    在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,如果有就返回成功;如果沒有,就檢查這兩種類型能否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這裏的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決衝突。主要是interface <-> interface(接口賦值給接口、接口轉換成另一接口)使用到動態生產itab

interface的結構如下:

接口類型的結構interfacetype

 1 type interfacetype struct {
 2     typ     _type   
 3     pkgpath name   //記錄定義接口的包名
 4     mhdr    []imethod  //一個imethod切片,記錄接口中定義的那些函數。
 5 }
 6 
 7 // imethod表示接口類型上的方法
 8 type imethod struct {
 9     name nameOff // name of method
10     typ  typeOff // .(*FuncType) underneath
11 }

  nameOff 和 typeOff 類型是 int32 ,這兩個值是鏈接器負責嵌入的,相對於可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。

4. 接口值的結構iface和eface

 為了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口。

 1 type iface struct { 
 2     tab  *itab
 3     data unsafe.Pointer
 4 }
 5 
 6 type eface struct {
 7     _type *_type
 8     data  unsafe.Pointer
 9 }
10 
11 type itab struct {
12     inter *interfacetype   //inter接口類型
13     _type *_type   //_type數據類型
14     hash  uint32  //_type.hash的副本。用於類型開關。 hash哈希的方法
15     _     [4]byte
16     fun   [1]uintptr  // 大小可變。 fun [0] == 0表示_type未實現inter。 fun函數地址佔位符
17 }

  iface結構體中的data是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,然後data指向這塊新的內存。

itab中的hash方法拷貝自_type.hash;fun是一個大小為1的uintptr數組,當fun[0]為0時,說明_type並沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了

4.1 全局的itab table

iface.go:

1 const itabInitSize = 512
2 
3 // 注意:如果更改這些字段,請在itabAdd的mallocgc調用中更改公式。
4 type itabTableType struct {
5     size    uintptr             // 條目數組的長度。始終為2的冪。
6     count   uintptr             // 當前已填寫的條目數。
7     entries [itabInitSize]*itab // really [size] large
8 }

  可以看出這個全局的itabTable是用數組在存儲的,size記錄數組的大小,總是2的次冪。count記錄數組中已使用了多少。entries是一個*itab數組,初始大小是512。

5. 接口類型轉換

  把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。

 1 func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
 2     if raceenabled {
 3         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
 4     }
 5     if msanenabled {
 6         msanread(elem, t.size)
 7     }
 8     x := mallocgc(t.size, t, true)
 9     // TODO: 我們分配一個清零的對象只是為了用實際數據覆蓋它。
10     //確定如何避免歸零。同樣在下面的convT2Eslice,convT2I,convT2Islice中。
11     typedmemmove(t, x, elem)
12     e._type = t
13     e.data = x
14     return
15 }
16 
17 func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
18     t := tab._type
19     if raceenabled {
20         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
21     }
22     if msanenabled {
23         msanread(elem, t.size)
24     }
25     x := mallocgc(t.size, t, true)
26     typedmemmove(t, x, elem)
27     i.tab = tab
28     i.data = x
29     return
30 }
31 
32 func convT2I16(tab *itab, val uint16) (i iface) {
33     t := tab._type
34     var x unsafe.Pointer
35     if val == 0 {
36         x = unsafe.Pointer(&zeroVal[0])
37     } else {
38         x = mallocgc(2, t, false)
39         *(*uint16)(x) = val
40     }
41     i.tab = tab
42     i.data = x
43     return
44 }
45 
46 func convI2I(inter *interfacetype, i iface) (r iface) {
47     tab := i.tab
48     if tab == nil {
49         return
50     }
51     if tab.inter == inter {
52         r.tab = tab
53         r.data = i.data
54         return
55     }
56     r.tab = getitab(inter, tab._type, false)
57     r.data = i.data
58     return
59 }

  可以看出:

  • 具體類型轉空接口,_type字段直接複製源的type;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 對於接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。

對於那些特定類型的值,如果是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]

5.1 接口轉接口

 1 func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
 2     tab := i.tab
 3     if tab == nil {
 4         return
 5     }
 6     if tab.inter != inter {
 7         tab = getitab(inter, tab._type, true)
 8         if tab == nil {
 9             return
10         }
11     }
12     r.tab = tab
13     r.data = i.data
14     b = true
15     return
16 }
17 
18 func assertE2I(inter *interfacetype, e eface) (r iface) {
19     t := e._type
20     if t == nil {
21         // 顯式轉換需要非nil接口值。
22         panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
23     }
24     r.tab = getitab(inter, t, false)
25     r.data = e.data
26     return
27 }
28 
29 func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
30     t := e._type
31     if t == nil {
32         return
33     }
34     tab := getitab(inter, t, true)
35     if tab == nil {
36         return
37     }
38     r.tab = tab
39     r.data = e.data
40     b = true
41     return
42 }

  我們看到有兩種用法:

  • 返回值是一個時,不能轉換就panic。
  • 返回值是兩個時,第二個返回值標記能否轉換成功

  此外,data複製的是指針,不會完整拷貝值。每次都malloc一塊內存,那麼性能會很差,因此,對於一些類型,golang的編譯器做了優化。

5.2 接口轉具體類型

  接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個empty interface轉換成具體類型的例子:

 1 var EFace interface{}
 2 var j int
 3 
 4 func F4(i int) int{
 5     EFace = I
 6     j = EFace.(int)
 7     return j
 8 }
 9 
10 func main() {
11     F4(10)
12 }

反彙編:

  go build -gcflags ‘-N -l’ -o tmp build.go

  go tool objdump -s “main.F4” tmp

  可以看彙編代碼:

1 MOVQ main.EFace(SB), CX       //CX = EFace.typ
2 LEAQ type.*+60128(SB), DX    //DX = &type.int
3 CMPQ DX, CX.                         //if DX == AX

  可以看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是不是同一個type,而不是調用某個函數在運行時動態對比。

5.3 非空接口類型轉換

 1 var tf Tester
 2 var t testStruct
 3 
 4 func F4() int{
 5     t := tf.(testStruct)
 6     return t.i
 7 }
 8 
 9 func main() {
10     F4()
11 }
12 //反彙編
13 MOVQ main.tf(SB), CX   // CX = tf.tab(.inter.typ)
14 LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ)
15 CMPQ DX, CX //

  可以看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是不是同一個itab,而不是調用某個函數在運行時動態對比。

6. 獲取itab的流程

  golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。看下代碼:

 1 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
 2     if len(inter.mhdr) == 0 {
 3         throw("internal error - misuse of itab")
 4     }
 5 
 6     // 簡單的情況
 7     if typ.tflag&tflagUncommon == 0 {
 8         if canfail {
 9             return nil
10         }
11         name := inter.typ.nameOff(inter.mhdr[0].name)
12         panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
13     }
14 
15     var m *itab
16 
17     //首先,查看現有表以查看是否可以找到所需的itab。
18     //這是迄今為止最常見的情況,因此請不要使用鎖。
19     //使用atomic確保我們看到該線程完成的所有先前寫入更新itabTable字段(在itabAdd中使用atomic.Storep)。
20     t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
21     if m = t.find(inter, typ); m != nil {
22         goto finish
23     }
24 
25     // 未找到。抓住鎖,然後重試。
26     lock(&itabLock)
27     if m = itabTable.find(inter, typ); m != nil {
28         unlock(&itabLock)
29         goto finish
30     }
31 
32     // 條目尚不存在。進行新輸入並添加。
33     m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
34     m.inter = inter
35     m._type = typ
36     m.init()
37     itabAdd(m)
38     unlock(&itabLock)
39 finish:
40     if m.fun[0] != 0 {
41         return m
42     }
43     if canfail {
44         return nil
45     }
46     //僅當轉換時才會發生,使用ok形式已經完成一次,我們得到了一個緩存的否定結果。
47     //緩存的結果不會記錄,缺少接口函數,因此初始化再次獲取itab,以獲取缺少的函數名稱。
48     panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
49 }

  流程如下:

  • 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是為了防止查找過程中,itabTable被替換導致查找錯誤。
  • 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因為在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
  • 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。

  這裏我們可以看到申請新的itab空間時,內存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因為字段fun是一個大小為1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。

6.1 在itabTable中查找itab find

 1 func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
 2     // 編譯器為我們提供了一些很好的哈希碼。
 3     return uintptr(inter.typ.hash ^ typ.hash)
 4 }
 5 
 6    // find在t中找到給定的接口/類型對。
 7    // 如果不存在給定的接口/類型對,則返回nil。
 8 func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
 9     // 使用二次探測實現。
10      //探測順序為h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k。
11      //我們保證使用此探測序列擊中所有表條目。
12     mask := t.size - 1
13     h := itabHashFunc(inter, typ) & mask
14     for i := uintptr(1); ; i++ {
15         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
16         // 在這裏使用atomic read,所以如果我們看到m!= nil,我們也會看到m字段的初始化。
17         // m := *p
18         m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
19         if m == nil {
20             return nil
21         }
22         if m.inter == inter && m._type == typ {
23             return m
24         }
25         h += I
26         h &= mask
27     }
28 }

  從註釋可以看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。以前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中我們看到有了改進。

6.2 檢查並生成itab init

 1 // init用所有代碼指針填充m.fun數組m.inter / m._type對。 如果該類型未實現該接口,將m.fun [0]設置為0,並返回缺少的接口函數的名稱。
 2 //可以在同一m上多次調用此函數,即使同時調用也可以。
 3 func (m *itab) init() string {
 4     inter := m.inter
 5     typ := m._type
 6     x := typ.uncommon()
 7 
 8     // inter和typ都有按名稱排序的方法,
 9      //並且接口名稱是唯一的,
10      //因此可以在鎖定步驟中對兩者進行迭代;
11      //循環是O(ni + nt)而不是O(ni * nt)。
12     ni := len(inter.mhdr)
13     nt := int(x.mcount)
14     xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
15     j := 0
16 imethods:
17     for k := 0; k < ni; k++ {
18         i := &inter.mhdr[k]
19         itype := inter.typ.typeOff(i.ityp)
20         name := inter.typ.nameOff(i.name)
21         iname := name.name()
22         ipkg := name.pkgPath()
23         if ipkg == "" {
24             ipkg = inter.pkgpath.name()
25         }
26         for ; j < nt; j++ {
27             t := &xmhdr[j]
28             tname := typ.nameOff(t.name)
29             if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
30                 pkgPath := tname.pkgPath()
31                 if pkgPath == "" {
32                     pkgPath = typ.nameOff(x.pkgpath).name()
33                 }
34                 if tname.isExported() || pkgPath == ipkg {
35                     if m != nil {
36                         ifn := typ.textOff(t.ifn)
37                         *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
38                     }
39                     continue imethods
40                 }
41             }
42         }
43         // didn't find method
44         m.fun[0] = 0
45         return iname
46     }
47     m.hash = typ.hash
48     return ""
49 }

  這個方法會檢查interface和type的方法是否匹配,即type有沒有實現interface。假如interface有n中方法,type有m中方法,那麼匹配的時間複雜度是O(n x m),由於interface、type的方法都按字典序排,所以O(n+m)的時間複雜度可以匹配完。在檢測的過程中,匹配上了,依次往fun字段寫入type中對應方法的地址。如果有一個方法沒有匹配上,那麼就設置fun[0]為0,在外層調用會檢查fun[0]==0,即type並沒有實現interface

  這裏我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環中,那麼就在那一層的循環上加個標籤,然後continue 標籤

6.3 把itab插入到itabTable中 itabAdd

 1 // itabAdd將給定的itab添加到itab哈希表中。
 2 //必須保持itabLock。
 3 func itabAdd(m *itab) {
 4     // 設置了mallocing時,錯誤可能導致調用此方法,通常是因為這是在恐慌時調用的。
 5     //可靠地崩潰,而不是僅在需要增長時崩潰哈希表。
 6     if getg().m.mallocing != 0 {
 7         throw("malloc deadlock")
 8     }
 9 
10     t := itabTable
11     if t.count >= 3*(t.size/4) { // 75% 負載係數
12         // 增長哈希表。
13         // t2 = new(itabTableType)+一些其他條目我們撒謊並告訴malloc我們想要無指針的內存,因為所有指向的值都不在堆中。
14         t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
15         t2.size = t.size * 2
16 
17         // 複製條目。
18         //注意:在複製時,其他線程可能會尋找itab和找不到它。沒關係,他們將嘗試獲取Itab鎖,因此請等到複製完成。
19         if t2.count != t.count {
20             throw("mismatched count during itab table copy")
21         }
22         // 發布新的哈希表。使用原子寫入:請參閱getitab中的註釋。
23         atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
24         // 採用新表作為我們自己的表。
25         t = itabTable
26         // 注意:舊錶可以在此處進行GC處理。
27     }
28     t.add(m)
29 }
30 // add將給定的itab添加到itab表t中。
31 //必須保持itabLock。
32 func (t *itabTableType) add(m *itab) {
33     //請參閱註釋中的有關探查序列的註釋。
34     //將新的itab插入探針序列的第一個空位。
35     mask := t.size - 1
36     h := itabHashFunc(m.inter, m._type) & mask
37     for i := uintptr(1); ; i++ {
38         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
39         m2 := *p
40         if m2 == m {
41             //給定的itab可以在多個模塊中使用並且由於全局符號解析的工作方式,
42             //指向itab的代碼可能已經插入了全局“哈希”。
43             return
44         }
45         if m2 == nil {
46             // 在這裏使用原子寫,所以如果讀者看到m,它也會看到正確初始化的m字段。
47             // NoWB正常,因為m不在堆內存中。
48             // *p = m
49             atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
50             t.count++
51             return
52         }
53         h += I
54         h &= mask
55     }
56 }

  可以看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。然後使itabTable指向新的表,最後把新的itab插入到新表中。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Non-local Neural Networks 原理詳解及自注意力機制思考

Paper:
 

Author:Xiaolong Wang, Ross Girshick, Abhinav Gupta, Kaiming He (CMU, FAIR)

1 創新點

這篇文章非常重要,個人認為應該算是cv領域裏面的自注意力機制的核心文章,語義分割裏面引入的各種自注意力機制其實都可以認為是本文的特殊化例子。分析本文的意義不僅僅是熟悉本文,而是了解其泛化思想。

不管是cv還是NLP任務,都需要捕獲長範圍依賴。在時序任務中,RNN操作是一種主要的捕獲長範圍依賴手段,而在CNN中是通過堆疊多個卷積模塊來形成大感受野。目前的卷積和循環算子都是在空間和時間上的局部操作,長範圍依賴捕獲是通過重複堆疊,並且反向傳播得到,存在3個不足:

(1) 捕獲長範圍依賴的效率太低;

(2) 由於網絡很深,需要小心的設計模塊和梯度;

(3) 當需要在比較遠位置之間來回傳遞消息時,這是局部操作是困難的.

故作者基於圖片濾波領域的非局部均值濾波操作思想,提出了一個泛化、簡單、可直接嵌入到當前網絡的非局部操作算子,可以捕獲時間(一維時序信號)、空間(圖片)和時空(視頻序列)的長範圍依賴。這樣設計的好處是:

(1) 相比較於不斷堆疊卷積和RNN算子,非局部操作直接計算兩個位置(可以是時間位置、空間位置和時空位置)之間的關係即可快速捕獲長範圍依賴,但是會忽略其歐式距離,這種計算方法其實就是求自相關矩陣,只不過是泛化的自相關矩陣

(2) 非局部操作計算效率很高,要達到同等效果,只需要更少的堆疊層

(3) 非局部操作可以保證輸入尺度和輸出尺度不變,這種設計可以很容易嵌入到目前的網絡架構中。

2 核心思想

由於我主要做2d圖片的CV需求,故本文的大部分分析都是針對圖片而言,而不是時間序列或者視頻序列。

本文的非局部操作算子是基於非局部均值操作而提出的,故很有必要解釋下非局部均值操作。我們在CNN或者傳統圖片濾波算子中涉及的都是局部操作,例如Sobel算子,均值濾波算子等等,其計算示意圖如下:

  圖片來源:吳恩達深度學習課程

可以看出每個位置的輸出值都是kernel和輸入的局部卷積計算得到的,而非局部均值濾波操作是: computes a weighted mean of all pixels in an image,非常簡單。核心思想是在計算每個像素位置輸出時候,不再只和鄰域計算,而是和圖像中所有位置計算相關性,然後將相關性作為一個權重表徵其他位置和當前待計算位置的相似度。可以簡單認為採用了一個和原圖一樣大的kernel進行卷積計算。下圖表示了高斯濾波,雙邊濾波和非局部均值處理過程:

 

可以看出對於待計算的中心紅色點,前兩種局部操作都是在鄰域計算,而非局部均值是和整個圖片進行計算的。但是實際上如果採用逐點計算方式,不僅計算速度非常慢,而且抗干擾能力不太好,故非局部均值操作是採用Block的思想,計算block和block之間的相關性。

 

可以看出,待計算的像素位置是p,故先構造block,然後計算其他位置block和當前block的相關性,可以看出q1和q2區域和q非常相似,故計算時候給予一個大權重,而q3給予一個小的權重。這樣的做法可以突出共性(關心的區域),消除差異(通常是噪聲)。

 
上圖可以看出非局部操作的優點,每一個例子中左圖是待計算像素點的位置,右圖是基於NL均值操作計算出來的權重分布圖,看(c)可以非常明顯看出,由於待計算點位置是在邊緣處,通過非局部操作后突出了全部邊緣。

上面的所有分析都是基於非局部操作來講的,但是實際上在深度學習時代,可以歸為自注意力機制Self-attention。在機器翻譯中,自我注意模塊通過關注所有位置並在嵌入空間中取其加權平均值來計算序列(例如,句子)中的位置處的響應,在CV中那就是通過關注圖片中(可以是特徵圖)所有位置並在嵌入空間中取其加權平均值來表示圖片中某位置處的響應。嵌入空間可以認為是一個更抽象的圖片空間表達,目的是匯聚更多的信息,提高計算效率。聽起來非常高級的樣子,到後面可以看出,是非常簡單的。

3 網絡結構

下面開始給出非局部操作的具體公式。首先在深度學習中非局部操作可以表達為:

 

 

 

i是輸出特徵圖的其中一個位置,通用來說這個位置可以是時間、空間和時空。j是所有可能位置的索引,x是輸入信號,可以是圖像、序列和視頻,通常是特徵圖。y是和x尺度一樣的輸出圖,f是配對計算函數,計算第i個位置和其他所有位置的相關性,g是一元輸入函數,目的是進行信息變換,C(x)是歸一化函數,保證變換前後整體信息不變。以上是一個非常泛化的公式,具體細節見下面。在局部卷積算子中,一般的  

由於f和g都是通式,故結合神經網絡特定,需要考慮其具體形式。
首先g由於是一元輸出,比較簡單,我可以採用1×1卷積,代表線性嵌入,其形式為:

 

對於f,前面我們說過其實就是計算兩個位置的相關性,那麼第一個非常自然的函數是Gaussian。

(1) Gaussian

 

對兩個位置進行點乘,然後通過指數映射,放大差異。

(2) Embedded Gaussian

 

前面的gaussian形式是直接在當前空間計算,而(2)更加通用,在嵌入空間中計算高斯距離。這裏:

   

 

 

前面兩個:  

 

 

仔細觀察,如果把C(x)考慮進去,那麼  

其實就是softmax形式,完整考慮是:

 

這個就是目前常用的位置注意力機制的表達式,所以說語義分割中大部分通道注意力機制都是本文的特殊化。

(3) Dot product

考慮一種最簡單的非局部操作形式:

 

其中C(x)=N,像素個數。可以看出(2) (3)的主要區別是是否含有激活函數softmax。

(4) Concatenation

參考 Relation Networks可以提出:

 

 

 

前面是基本的非局部操作算子,利用這些算子,下面開始構造成模塊。  

可以看出,上面構造成了殘差形式。上面的做法的好處是可以隨意嵌入到任何一個預訓練好的網絡中,因為只要設置W_z初始化為0,那麼就沒有任何影響,然後在遷移學習中學習新的權重。這樣就不會因為引入了新的模塊而導致預訓練權重無法使用

下面結合具體實例分析:

 

由於我們考慮的是圖片,故可以直接設置T=1,或者說不存在。首先網絡輸入是X= (batch, h, w, 1024) ,經過Embedded Gaussian中的兩個嵌入權重變換 , 得到(batch, h, w, 512), (batch, h, w, 512), 其實這裏的目的是降低通道數,減少計算量;然後分別對這兩個輸出進行reshape操作,變成(batch, hw, 512),后對這兩個輸出進行矩陣乘(其中一個要轉置),計算相似性,得到(batch, hw, hw),
然後在第2個維度即最後一個維度上進行softmax操作,得到(batch, hw, hw), 意這樣做就是通道注意力,相當於找到了當前圖片或特徵圖中每個像素與其他所有位置像素的歸一化相關性;然後將g也採用一樣的操作,先通道降維,然後reshape;然後和 (batch, hw, hw)進行矩陣乘,得到(batch, h, w, 512), 即將通道注意力機制應用到了所有通道的每張特徵圖對應位置上,本質就是輸出的每個位置值都是其他所有位置的加權平均值,通過softmax操作可以進一步突出共性。最後經過一個1×1卷積恢復輸出通道,保證輸入輸出尺度完全相同。

4 核心代碼實現

 

拷貝的代碼來源:

可以看出,具體實現非常簡單,就不細說了。

5 擴展

通讀全文,你會發現思路非常清晰,模塊也非常簡單。其背後的思想其實是自注意力機制的泛化表達,準確來說本文只提到了位置注意力機制(要計算位置和位置之間的相關性,辦法非常多)。

個人認為:如果這些自注意模塊的計算開銷優化的很小,那麼應該會成為CNN的基礎模塊。既然位置和位置直接的相關性那麼重要,那我是不是可以認為graph CNN才是未來?因為圖卷積網絡是基於像素點和像素點之間建模,兩者之間的權重是學習到的,性能肯定比這種自監督方式更好,後面我會寫文章分析。

本文設計的模塊依然存在以下的不足:

(1) 只涉及到了位置注意力模塊,而沒有涉及常用的通道注意力機制

(2) 可以看出如果特徵圖較大,那麼兩個(batch,hxw,512)矩陣乘是非常耗內存和計算量的,也就是說當輸入特徵圖很大存在效率底下問題,雖然有其他辦法解決例如縮放尺度,但是這樣會損失信息,不是最佳處理辦法。

 

 

6 實驗

Non-local Blocks的高效策略。我們設置Wg,,的channel的數目為x的channel數目的一半,這樣就形成了一個bottleneck,能夠減少一半的計算量。Wz再重新放大到x的channel數目,保證輸入輸出維度一致。 
還有一個subsampling的trick可以進一步使用,就是將(1)式變為:yi=1C(x^)∑∀jf(xi,x^j)g(x^j),其中x^x下採樣得到的(比如通過pooling),我們將這個方式在空間域上使用,可以減小1/4的pairwise function的計算量。這個trick並不會改變non-local的行為,而是使計算更加稀疏了。這個可以通過在圖2中的ϕg後面增加一個max pooling層實現。 
我們在本文中的所有non-local模塊中都使用了上述的高效策略。

6.1. 視頻分類模型

為了理解non-local networks的操作,我們在視頻分類任務上進行了一系列的ablation experiments。 
2D ConvNet baseline (C2D)。為了獨立開non-local nets中時間維度的影響vs 3D ConvNets,我們構造了一個簡單的2D baseline結構。 
Table 1給出了ResNet-50 C2D backbone。輸入的video clip是32幀,大小為224*224。Table 1中的所有卷積都是用的2D的kernel,即逐幀對輸入視頻進行計算。唯一和temporal有關的計算就是pooling,也就是說這個baseline模型簡單地在時間維度上做了一個聚合的操作。 

Inflated 3D ConvNet (I3D)。 Table 1中的C2D模型可以通過inflate的操作轉換成一個3D卷積的結構。具體地,一個2D k*k大小的kernel可以inflate成3D t*k*k大小的kernel,只要將其權重重複t次,再縮小t倍即可。

我們討論2種inflate的方式。一種是將residual block中的3*3的kernel inflate成3*3*3的,另一種是將residual block中的1*1的kernel inflate成3*1*1的。這兩種形式我們分別用I3D333I3D311表示。因為3D conv的計算量很大,我們只對每2個residual blocks中的1個kernel做inflate。對更多的kernel做inflate發現效果反而變差了。另外conv1層我們inflate成5*7*7。

Non-local network。 我們將non-local block插入到C2D或I3D中,就得到了non-local nets。我們研究了插入1,5,10個non-local blocks的情況,實現細節將在後面給出。

6.2 Non-local Network實現細節

Training。 我們的模型是在ImageNet上pretrain的,沒有特殊說明的話我們使用32幀的輸入。32幀是通過從原始長度的視頻中隨機選擇1個位置取出64個連續幀,然後每隔1幀取1幀得到的最終的32幀。spatial size是224*224大小,是將原始視頻rescale到短邊為[256,320]區間的隨機值,然後再random crop 224*224大小。我們在8卡GPU上進行訓練,每卡上有8 clips(也就是說總的batchsize是64 clips)。我們一共迭代了400k iterations,初始lr為0.01,然後每150k iterations lr下降1/10。momentum設為0.9,weight decay設為0.0001。dropout在global pooling層後面使用,dropout ratio設為0.5。 
我們finetune模型的時候 BN是打開的,這和常見的finetune ResNet的操作不同,它們通常是frozen BN。我們發現在我們的實驗中enable BN有利於減少過擬合。 
在最後一個1*1*1 conv層(表示Wz)的後面我們加了一個BN層,其他位置我們沒有增加BN。這個BN層的scale參數初始化為0,這是為了保證整個non-local block的初始狀態相當於一個identity mapping,這樣插入到任何預訓練網絡中在一開始都能保持其原來的表現。

Inference。 推理時,在我們將視頻rescale到短邊256進行推理。時域上我們從整個視頻中平均採樣10個clip,然後分別計算他們的softmax scores,最後做平均得到整個視頻的score。

6.3 實驗

關於視頻分類的實驗,我們在Kinetics上進行全面的實驗,另外也給出了Charades上的實驗結果,显示出我們的模型的泛化性。這裏只給出Kinetics上的結果,更多的請看原文。 
Table 2給出了ablation results。 

f的表現形式的影響。表2a比較了不同的non-local block的形式插入到C2D得到的結果(插入位置在res4的最後一個residual block之前)。發現即使只加一個non-local block都能得到~1%的提高。 
有意思的是不同的non-local block的形式效果差不多,說明是non-local block的結構在起作用,而對具體的表達方式不敏感。本文後面都採用embedded Gaussian進行實驗,因為這個版本有softmax,可以直接給出[0,1]之間的scores。

哪個階段加入non-local blocks?表2b比較了一個non-local block加在resnet的不同stage的效果,具體加在不同stage的最後一個residual block之前。發現在res2,res3,res4層上加non-local block效果類似,加在res5上效果稍差。這個的可能原因是res5的spatial size比較小,只有7*7,可能無法提供精確的spatial信息了。

加入更多的non-local blocks。表2c給出了加入更多non-local block的結果,我們在resnet-50上加1 block(在res4),加5 block(3個在res4,2個在res3,每隔1個residual block加1個non-local block),加10 block(在res3和res4每個residual block都加non-local block)。在resnet101的相同位置加block。發現更多non-local block通常有更好的結果。我們認為這是因為更多的non-local block能夠捕獲長距離多次轉接的依賴。信息可以在時空域上距離較遠的位置上進行來回傳遞,這是通過local models無法實現的。 
另外需要提到的是增加non-local block得到的性能提升並不只是因為它給base model增加了深度。為了說明這一點,表2c中resnet50 5blocks能夠達到73.8的acc,而resnet101 baseline是73.1,同時resnet50 5block只有resnet101的約70%的參數量和80%的FLOPs。說明non-local block得到的性能提升並不只是因為它增加了深度。

時空域上做non-local。我們的方法也可以處理時空域的信息,這一特性非常好,在視頻中相關的物體可能出現在較遠的空間和較長的時間,它們的相關性也可以被我們的模型捕獲。表2d給出了在時間維度,空間維度和時空維度分別做non-local的結果。僅在空間維度上做就相當於non-local的依賴僅在單幀圖像內部發生,也就是說在式(1)上僅對index i的相同幀的index j做累加。僅在時間維度上做也類似。表2d显示只做時間維度或者只做空間維度的non-local,都比C2D baseline要好,但是沒有同時做時空維度的效果好。

Non-local net vs. 3D ConvNet。表2e比較了我們的non-local C2D版本和inflated 3D ConvNets的性能。Non-local的操作和3D conv的操作可以看成是將C2D推廣到時間維度的兩種方式。 
表2e也比較了param的數量,FLOPs等。我們的non-local C2D模型比I3D更加精確(75.1 vs 74.4),並且有更小的FLOPs(1.2x vs 1.5x)。說明單獨使用時non-local比3D conv更高效。

Non-local 3D ConvNet. 不管上面的比較,其實non-local操作和3D conv各有各的優點:3D conv可以對局部依賴進行建模。表2f給出了在I3D311上插入5個non-local blocks的結果。發現NL I3D都能夠在I3D的基礎上提升1.6個點的acc,說明了non-local和3D conv是可以相互補充的。

更長的輸入序列。 最後我們也實驗了更長輸入序列的情況下模型的泛化性。輸入clip包含128幀連續幀,沒有做下採樣,是一般情況下取的32幀的4倍長度。為了將這個模型放入顯存中,每個GPU上只能放下2 clips。因為這麼小的batchsize的原因,我們freeze所有的BN層。我們從32幀訓練得到的模型作為初始化模型,然後用128幀進行finetune,使用相同的iterations數目(雖然batchsize減小了),初始lr為0.0025,其他設置和之前保持一致。 
表2g給出了128幀的實驗結果,和表2f的32幀的結果相比,所有模型都表現得更好,說明我們的模型在長序列上的效果也很好。

和state-of-the-art的比較。表3給出了Kinetics上各個方法的結果。 

 

 

來源: 

       https://www.jianshu.com/p/a9771abedf50

       https://blog.csdn.net/u010158659/article/details/78635219

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

南投搬家前需注意的眉眉角角,別等搬了再說!

ThreadLocal 源碼解析

在activeJDBC框架內部的實現中看到了 ThreadLocal 這個類,記錄下了每個線程獨有的連接

private static final ThreadLocal<HashMap<String, Connection>> connectionsTL = new ThreadLocal<>();

感覺是個知識點,就打開源碼看看了。先看一下源碼里的解釋

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

這個鳥文,瞎翻譯一下,就是:

這個類提供了供線程專享的變量。這些變量不同與其它普通的變量,它是每個線程都有一個自己的獨立初始化的變量(通過get和set方法實現)。這個類的實例常用於類的私有靜態字段,以實現每個線程都有自己的狀態(例如userId,事務ID等)。

先跑一下用法吧,

package com.test.threadlocal;

public class TestController {
    
    private static int index = 0;
    private static String str = "這個字符串是每個線程共享的";
    // 這個變量,看似是一個類的靜態屬性,實則是每個線程有自己獨有的區域
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "main線程專享";
        }
    };      
    
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 3; i++) {
            Thread t = new MyThread();
            t.start();
            t.join();
        }
        
        System.out.println(str);
        System.out.println(threadStr.get());
    }
    
    static class MyThread extends Thread{

        @Override
        public void run() {
            index++;
            str = "第" + index + "個str";
            threadStr.set("第" + index + "個threadStr");
        }
        
        
    }

}

這個例子中,從str和threadStr變量的打印結果可以看出來。str被所有的線程讀和寫,threadStr在每個線程內部開闢了一塊線程專享的區域。接下來,我們看一下具體實現。
先看一下構造函數

     /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();
     /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

構造函數是空的,但是,該類有一個私有整型常量threadLocalHashCodenextHashCode()方法我們就不看了,省的一如源碼深似海。看鳥文的話,大概就是每new一個ThreadLocal變量的時候,就會生成一個散列碼,該碼非極端情況下與某個整數取模后不容易衝突(這句話有點迷吧,其實我也不懂)
然後看一下set方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

容易看出,這個方法設置每個線程自己的value,相當於當前線程是key,然後得出一個ThreadLocalMap。顯然,這個map用來保存線程內部的值,既然是map當然每個線程可以保存多個數值了,該map的value我們猜一下就是我要保存的具體的值,估計是用Object類聲明的。那key是什麼呢?我們看下ThreadLocalMap類的構造方法。

/**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
                
/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

我的天!這個類沒有繼承我們想象中的HashMap,或者是ConcurrentMap,但是看過,Map的內部實現的同學應該可以發現,這個Map的實現和HashMap的實現簡直就是小巫見大巫,有沒有。它在構造函數中做了如下幾步:

  1. 初始化一個大小為16的Entry數組
  2. 通過上面說過的很迷的HashCode散列值與15取模得到將要存儲在數組中的索引值
  3. 構造Entry,然後保存進去
  4. 長度設置為1
  5. 設置要擴容的限制大小為16的2/3

我們看到這個不就是用數組實現的Map嘛,看過HashMap實現的我們,覺得洒洒水啦。
Map的set和get方法就不分析了。ThreadLocal的get方法我們還是要貼出來的,畢竟是我們主要分析的東西

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可見,是獲取到當前線程,用作key獲取到Map,然後用當前this獲取到Entry實體。最後當然獲取到了存儲的value。

我編碼,我快樂~

本文由博客一文多發平台 發布!

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=19l4zaaz7g6ct

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

本田計畫到2030年新能源汽車比例將達到三分之二

據《財富》網路版報導,本田週三表示,計畫把新能源汽車比例由當前的5%提高到2030年的三分之二。

本田在發佈的最新戰略中稱,在未來不到15年,油田混合動力、插電式混合動力、電池動力和燃料電池動力汽車產量將超過燃油動力汽車。

本田首席執行官八鄉隆弘(Takahiro Hachigo)說,插電式混合動力汽車將是公司“未來電氣化的核心”。

八鄉隆弘說,本田將於2018年在北美推出一款插電式混合動力汽車,然後會推出主要車型的插電式版本,並增加車型數量。
 

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Tim Cook:汽車是蘋果給用戶準備的聖誕禮物

在 2016 年蘋果公司股東大會上,該公司 CEO Tim Cook 在回答股東關於蘋果汽車的詢問時,他表示用戶等待這一款新品就是等待聖誕禮物,現在距離耶誕節還有一段時間。蘋果公司已經從 Tesla、Ford、GM、Samsung 等公司招募了數百名工程師,組成了電動車研發團隊。

蘋果正在研發電動車已經不是什麼新聞了,但一直沒有官方消息證實,該公司 CEO Tim Cook 在股東大會上回應了股東有關蘋果汽車的問題,他表示現在就像兒童時代,等待平安夜的時間,期待將收到什麼禮物,未來一段時間,就像是等待禮物的平安夜。這意味蘋果公司正在研發電動汽車,但上市還需要等一段時間。   除了電動汽車外,此次股東大會還討論其他議題,蘋果在 2016 的研發支出為 85 億美元,並收購了 19 家公司,目前該公司的員工總數大約為 11.6 萬人。關於近日廣受關注的 FBI 要求解鎖 iPhone 的事件,Tim Cook 再次重申了蘋果公司立場,堅決擁護用戶的個人隱私和資訊安全。蘋果公司的新總部即將完工,大約在 2017 年 1 月部分員工就將搬入新的員工,這是 Steve Jobs 生前最關注的專案之一。

(首圖來源: CC BY 2.0)    (本文授權轉載自《》─〈〉)

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Battery Japan – EV應用搶眼

Battery Japan 2016 已進行了七年,雖然依舊是設備為主軸,但散布在其中的攤位中還是可見一些重要應用,根據集邦科技綠能事業部的觀察,除了固態電池之外,也包括了為數不少的車用電池模組系統商,讓參展應用顯得更多元化,尤其豐田旗下的專屬電池廠Primearth EV Energy 更是備受矚目。

已參展多屆的Eliiy power依然集中在鋰鐵電池,本次首次展出了在機車的應用,選擇該類應用的重點,還是在於彰顯該類電池的高安全與可靠性。看好家用儲能市場的發展所產生的多樣化需求,本次除了過去標榜的中大型儲能系統外,本次也展出了在家用小型儲能系統。

可撓式穿戴裝置近年大受消費市場的關注,固態電池供應商之一的Hitz也在本次展覽展出更精良的性能表現,目前該類產品的環境溫度已可承受-40~100度的高溫差,預料在工業相關應用將有機會率先導入。

Isabellenhütte Heusler 也是德國公司,本業專注在被動元件與銅質電池連接片,近來也跨足到連接片集成的應用(右下圖),透過高精度的溫度與電壓偵測,可滿足使用環境屬於路線不確定性較高的乘用車輛,提高駕駛對於車輛狀況的精確掌握。因位可提供客製化設計,該類產品目前已被歐洲Tier1 所採用,至於亞洲市場則是更關注在標準化集成元件的量產化進度。

FEV為德國驅動系統元件相關,主要提供車廠在投入PHEV所需的硬體,也因此客戶需求多集中在歐洲,解決車廠在驅動與儲能系統的工程問題,PHEV相較HEV需要更高的電池容量,且要兼容技有的引擎系統,在配置上以中置環境為首選。

Primearth EV Energy第一次參展覽,備受關注的原因除了是豐田注資外,90%的電池芯業務也是與豐田相關,本次展出了豐田HEV從鎳氫電池與鋰電池,都是屬於大功率輸出的系統,甚至是正在進行的FCEV也是由Primearth EV Energy所供應。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

南投搬家前需注意的眉眉角角,別等搬了再說!

[springboot 開發單體web shop] 6. 商品分類和輪播廣告展示

目錄

商品分類&輪播廣告

因最近又被困在了OSGI技術POC,更新進度有點慢,希望大家不要怪罪哦。

我們實現了登錄之後前端的展示,如:

接着,我們來實現左側分類欄目的功能。

商品分類|ProductCategory

從上圖我們可以看出,商品的分類其實是有層級關係的,而且這種關係一般都是無限層級。在我們的實現中,為了效果的展示,我們僅僅是展示3級分類,在大多數的中小型電商系統中,三級分類完全足夠應對SKU的分類。

需求分析

先來分析分類都包含哪些元素,以jd為例:

  • logo(logo) 有的分類文字前面會有小標
  • 分類展示主圖(img_url)
  • 主標題(title)
  • 副標題/Slogan
  • 圖片跳轉地址(img_link_url)– 大多數時候我們點擊分類都會分類Id跳轉到固定的分類商品列表展示頁面,但是在一些特殊的場景,比如我們要做一個活動,希望可以點擊某一個分類的主圖直接定位到活動頁面,這個url就可以使用了。
  • 上級分類(parent_id)
  • 背景色(bg_color)
  • 順序(sort)
  • 當前分類級別(type)

開發梳理

在上一小節,我們簡單分析了一下要實現商品分類的一些points,那麼我們最好在每次拿到需求【開發之前】,對需求進行拆解,然後分解開發流程,這樣可以保證我們更好的理解需求,以及在開發之前發現一部分不合理的需求,並且如果需求設計不合理的話,開發人員完全有權,也有責任告知PM。大家的終極目的都是為了我們做的產品更加合理,好用,受歡迎!

  • 首次展示,僅僅讀取一級分類(Root)
  • 根據一級分類查詢二三級子分類

編碼實現

查詢一級分類

Service實現

1.在com.liferunner.service中創建service 接口ICategoryService.java, 編寫查詢所有一級分類的方法getAllRootCategorys,如下:

package com.liferunner.service;
import com.liferunner.dto.CategoryResponseDTO;
import com.liferunner.dto.SecondSubCategoryResponseDTO;
import java.util.List;
/**
 * ICategoryService for : 分類service
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/13
 */
public interface ICategoryService {
    /**
     * 獲取所有有效的一級分類(根節點)
     *
     * @return
     */
    List<CategoryResponseDTO> getAllRootCategorys();
}

2.編寫實現類com.liferunner.service.ICategoryService.java

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;
    
    @Override
    public List<CategoryResponseDTO> getAllRootCategorys() {
        Example example = new Example(Category.class);
        val conditions = example.createCriteria();
        conditions.andEqualTo("type", CategoryTypeEnum.ROOT.type);
        val categoryList = this.categoryMapper.selectByExample(example);
        //聲明返回對象
        List<CategoryResponseDTO> categoryResponseDTOS = new ArrayList<>();
        if (!CollectionUtils.isEmpty(categoryList)) {
            //賦值
            CategoryResponseDTO dto;
            for (Category category : categoryList) {
                dto = new CategoryResponseDTO();
                BeanUtils.copyProperties(category, dto);
                categoryResponseDTOS.add(dto);
            }
        }
        return categoryResponseDTOS;
    }
}

上述代碼很好理解,創建tk.mybatis.mapper.entity.Example,將條件傳入,然後使用通用Mapper查詢到type=1的一級分類,接着將查到的對象列錶轉換為DTO對象列表。

Controller實現

一般情況下,此類查詢都會出現在網站的首頁,因此我們來創建一個com.liferunner.api.controller.IndexController,並對外暴露一個查詢一級分類的接口:

package com.liferunner.api.controller;

import com.liferunner.service.ICategoryService;
import com.liferunner.service.IProductService;
import com.liferunner.service.ISlideAdService;
import com.liferunner.utils.JsonResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import java.util.Collections;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
 * IndexController for : 首頁controller
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/12
 */
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;

    @GetMapping("/rootCategorys")
    @ApiOperation(value = "查詢一級分類", notes = "查詢一級分類")
    public JsonResponse findAllRootCategorys() {
        log.info("============查詢一級分類==============");
        val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============一級分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
}
Test API

編寫完成之後,我們需要對我們的代碼進行測試驗證,還是通過使用RestService插件來實現,當然,大家也可以通過Postman來測試。

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 1,
      "name": "煙酒",
      "type": 1,
      "parentId": 0,
      "logo": "img/cake.png",
      "slogan": "吸煙受害健康",
      "catImage": "http://www.life-runner.com/shop/category/cake.png",
      "bgColor": "#fe7a65"
    },
    {
      "id": 2,
      "name": "服裝",
      "type": 1,
      "parentId": 0,
      "logo": "img/cookies.png",
      "slogan": "我選擇我喜歡",
      "catImage": "http://www.life-runner.com/shop/category/cookies.png",
      "bgColor": "#f59cec"
    },
    {
      "id": 3,
      "name": "鞋帽",
      "type": 1,
      "parentId": 0,
      "logo": "img/meat.png",
      "slogan": "飛一般的感覺",
      "catImage": "http://www.life-runner.com/shop/category/meat.png",
      "bgColor": "#b474fe"
    }
  ],
  "ok": true
}

根據一級分類查詢子分類

因為根據一級id查詢子分類的時候,我們是在同一張表中做自連接查詢,因此,通用mapper已經不適合我們的使用,因此我們需要自定義mapper來實現我們的需求。

自定義Mybatis Mapper實現

在之前的編碼中,我們都是使用的插件幫我們實現的通用Mapper,但是這種查詢只能處理簡單的單表CRUD,一旦我們需要SQL 包含一部分邏輯處理的時候,那就必須得自己來編寫了,let’s code.
1.在項目mscx-shop-mapper中,創建一個新的custom package,在該目錄下創建自定義mappercom.liferunner.custom.CategoryCustomMapper

public interface CategoryCustomMapper {
    List<SecondSubCategoryResponseDTO> getSubCategorys(Integer parentId);
}

2.resources目錄下創建目錄mapper.custom,以及創建和上面的接口相同名稱的XML文件mapper/custom/CategoryCustomMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liferunner.custom.CategoryCustomMapper">
    <resultMap id="subCategoryDTO" type="com.liferunner.dto.SecondSubCategoryResponseDTO">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="type" jdbcType="INTEGER" property="type"/>
        <result column="parentId" jdbcType="INTEGER" property="parentId"/>
        <collection property="thirdSubCategoryResponseDTOList" ofType="com.liferunner.dto.ThirdSubCategoryResponseDTO">
            <id column="subId" jdbcType="INTEGER" property="subId"/>
            <result column="subName" jdbcType="VARCHAR" property="subName"/>
            <result column="subType" jdbcType="INTEGER" property="subType"/>
            <result column="subParentId" jdbcType="INTEGER" property="subParentId"/>
        </collection>
    </resultMap>
    <select id="getSubCategorys" resultMap="subCategoryDTO" parameterType="INTEGER">
        SELECT p.id as id,p.`name` as `name`,p.`type` as `type`,p.father_id as parentId,
        c.id as subId,c.`name` as subName,c.`type` as subType,c.parent_id as subParentId
        FROM category p
        LEFT JOIN category c
        ON p.id = c.parent_id
        WHERE p.parent_id = ${parentId};
    </select>
</mapper>

TIPS
上述創建的package,一定要在項目的啟動類com.liferunner.api.ApiApplication中修改@MapperScan(basePackages = { "com.liferunner.mapper", "com.liferunner.custom"}),如果不把我們的custom package加上,會造成掃描不到而報錯。

在上面的xml中,我們定義了兩個DTO對象,分別用來處理二級和三級分類的DTO,實現如下:

@Data
@ToString
public class SecondSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer id;

    /**
     * 分類名稱
     */
    private String name;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer type;

    /**
     * 父id
     */
    private Integer parentId;

    List<ThirdSubCategoryResponseDTO> thirdSubCategoryResponseDTOList;
}
---
    
@Data
@ToString
public class ThirdSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer subId;

    /**
     * 分類名稱
     */
    private String subName;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer subType;

    /**
     * 父id
     */
    private Integer subParentId;
}
Service實現

編寫完自定義mapper之後,我們就可以繼續編寫service了,在com.liferunner.service.ICategoryService中新增一個方法:getAllSubCategorys(parentId).如下:

public interface ICategoryService {
    ...
    /**
     * 根據一級分類獲取子分類
     *
     * @param parentId 一級分類id
     * @return 子分類list
     */
    List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId);
}

com.liferunner.service.impl.CategorySericeImpl實現上述方法:

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private CategoryCustomMapper categoryCustomMapper;
    ...
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId) {
        return this.categoryCustomMapper.getSubCategorys(parentId);
    }
}
Controller實現
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;
    ...
    @GetMapping("/subCategorys/{parentId}")
    @ApiOperation(value = "查詢子分類", notes = "根據一級分類id查詢子分類")
    public JsonResponse findAllSubCategorys(
            @ApiParam(name = "parentId", value = "一級分類id", required = true)
            @PathVariable Integer parentId) {
        log.info("============查詢id = {}的子分類==============", parentId);
        val categoryResponseDTOS = this.categoryService.getAllSubCategorys(parentId);
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============子分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
}
Test API
{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 11,
      "name": "國產",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 37,
          "subName": "中華",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 38,
          "subName": "冬蟲夏草",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 39,
          "subName": "南京",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 40,
          "subName": "雲煙",
          "subType": 3,
          "subParentId": 11
        }
      ]
    },
    {
      "id": 12,
      "name": "外煙",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 44,
          "subName": "XXXXX",
          "subType": 3,
          "subParentId": 12
        },
        {
          "subId": 45,
          "subName": "RRRRR",
          "subType": 3,
          "subParentId": 12
        }
      ]
    }
  ],
  "ok": true
}

以上我們就已經實現了和jd類似的商品分類的功能實現。

輪播廣告|SlideAD

需求分析

這個就是jd或者tb首先的最頂部的廣告圖片是一樣的,每隔1秒自動切換圖片。接下來我們分析一下輪播圖中都包含哪些信息:

  • 圖片(img_url)是最基本的
  • 圖片跳轉連接(img_link_url),這個是在我們點擊這個圖片的時候需要跳轉到的頁面
  • 有的可以直接跳轉到商品詳情頁面
  • 有的可以直接跳轉到某一分類商品列表頁面
  • 輪播圖的播放順序(sort)

開發梳理

直接查詢出所有的有效的輪播圖片,並且進行排序

編碼實現

Service 實現

和商品分類實現一樣,在mscx-shop-service中創建com.liferunner.service.ISlideAdService並實現,代碼如下:

public interface ISlideAdService {
    /**
     * 查詢所有可用廣告並排序
     * @param isShow
     * @return
     */
    List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking);
}
@Service
@Slf4j
public class SlideAdServiceImpl implements ISlideAdService {

    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }

    @Override
    public List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking) {
        Example example = new Example(SlideAds.class);
        //設置排序
        if (StringUtils.isBlank(sortRanking)) {
            example.orderBy("sort").asc();
        } else {
            example.orderBy("sort").desc();
        }
        val conditions = example.createCriteria();
        conditions.andEqualTo("isShow", isShow);
        val slideAdsList = this.slideAdsMapper.selectByExample(example);
        //聲明返回對象
        List<SlideAdResponseDTO> slideAdResponseDTOList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(slideAdsList)) {
            //賦值
            SlideAdResponseDTO dto;
            for (SlideAds slideAds : slideAdsList) {
                dto = new SlideAdResponseDTO();
                BeanUtils.copyProperties(slideAds, dto);
                slideAdResponseDTOList.add(dto);
            }
        }
        return slideAdResponseDTOList;
    }
}

從上述可以看到,這裏我使用的是構造函數注入SlideAdsMapper,其餘代碼單表查詢沒什麼特別的,根據條件查詢輪播圖,並返回結果,返回的對象是com.liferunner.dto.SlideAdResponseDTO列表,代碼如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "輪播廣告返回DTO", description = "輪播廣告返回DTO")
public class SlideAdResponseDTO{
    /**
     * 主鍵
     */
    private String id;

    /**
     * 圖片地址
     */
    private String imageUrl;

    /**
     *  背景顏色
     */
    private String backgroundColor;

    /**
     * 商品id
     */
    private String productId;

    /**
     * 商品分類id
     */
    private String catId;

    /**
     * 圖片跳轉URL
     */
    private String imageLinkUrl;

    /**
     * 輪播圖類型 用於判斷,可以根據商品id或者分類進行頁面跳轉,1:商品 2:分類 3:鏈接url
     */
    private Integer type;

    /**
     * 輪播圖展示順序 輪播圖展示順序,從小到大
     */
    private Integer sort;

    /**
     * 是否展示 是否展示,1:展示    0:不展示
     */
    private Integer isShow;

    /**
     * 創建時間 創建時間
     */
    private Date createTime;

    /**
     * 更新時間 更新
     */
    private Date updateTime;
}

Controller實現

com.liferunner.api.controller.IndexController中,新添加一個查詢輪播圖API,代碼如下:

    @Autowired
    private ISlideAdService slideAdService;

    @GetMapping("/slideAds")
    @ApiOperation(value = "查詢輪播廣告", notes = "查詢輪播廣告接口")
    public JsonResponse findAllSlideList() {
        log.info("============查詢所有輪播廣告,isShow={},sortRanking={}=============="
                , 1, "desc");
        val slideAdsList = this.slideAdService.findAll(1, "desc");
        if (CollectionUtils.isEmpty(slideAdsList)) {
            log.info("============未查詢到任何輪播廣告==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============輪播廣告查詢result:{}=============="
                , slideAdsList);
        return JsonResponse.ok(slideAdsList);
    }

Test API

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": "slide-100002",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmH6AeuRrAAEZviPhyQ0768.png",
      "backgroundColor": "#55be59",
      "productId": "",
      "catId": "133",
      "type": 2,
      "sort": 2,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    },
    {
      "id": "slide-100003",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmHuAPlXvAAFe-H5_-Nw961.png",
      "backgroundColor": "#ff9801",
      "productId": "y200008",
      "catId": "",
      "type": 1,
      "sort": 1,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    }
  ],
  "ok": true
}

福利講解

在我們的實現代碼中,有心的同學可以看到,我使用了3種不同的Bean注入方式:

  • 屬性注入
    @Autowired
    private ISlideAdService slideAdService;
  • 構造函數注入
    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }
  • Lombok插件注入(本質也是構造器注入,代碼會動態生成。)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ProductServiceImpl implements IProductService {
    // RequiredArgsConstructor 構造器注入
    private final ProductCustomMapper productCustomMapper;
    private final ProductsMapper productsMapper;
    ...
}

那麼,這幾種注入都有什麼區別呢?首先我們下了解一下Spring的注入是干什麼的?

Spring提出了依賴注入的思想,即依賴類不由程序員實例化,而是通過Spring容器幫我們new指定實例並且將實例注入到需要該對象的類中。
依賴注入的另一種說法是”控制反轉”。通俗的理解是:平常我們new一個實例,這個實例的控制權是我們程序員, 而控制反轉是指new實例工作不由我們程序員來做而是交給Spring容器來做。

在傳統的SpringMVC中,大家使用的都是XML注入,比如:

<!--配置bean,配置后該類由spring管理--> 
<bean name="CategorySericeImpl" class="com.liferunner.service.impl.CategorySericeImpl"> 
<!--注入配置當前類中相應的屬性--> 
<property name="categoryMapper" ref="categoryMapper"></property> 
</bean> 
<bean name="categoryMapper" class="com.liferunner.mapper.CategoryMapper"></bean>

注入之後,使用@Autowired,我們可以很方便的自動從IOC容器中查找屬性,並返回。

@Autowired的原理
在啟動spring IoC時,容器自動裝載了一個AutowiredAnnotationBeanPostProcessor後置處理器,當容器掃描到@Autowied、@Resource或@Inject時,就會在IoC容器自動查找需要的bean,並裝配給該對象的屬性。
注意事項:
在使用@Autowired時,首先在容器中查詢對應類型的bean
如果查詢結果剛好為一個,就將該bean裝配給@Autowired指定的數據
如果查詢的結果不止一個,那麼@Autowired會根據名稱來查找。
如果查詢的結果為空,那麼會拋出異常。解決方法時,使用required=false

上述三種註解方式,其實本質上還是注入的2種:set屬性注入 & 構造器注入,使用方式都可以,根據個人喜好來用,本人喜歡使用lombok插件注入是因為,它將代碼整合在一起,更加符合我們Spring自動注入的規範。

源碼下載

下節預告

下一節我們將繼續開發我們電商的核心部分-商品列表和詳情展示,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!