Spring Boot路由id轉(zhuǎn)化為控制器Entity參數(shù)

問題

開發(fā)restful api,大部分時(shí)候都要實(shí)現(xiàn)根據(jù)id獲取對(duì)象的api甜无,一般來說代碼是這樣的

class UserController {
    @Autowired
    UserService userService;

    @GetMapping("/{id}")
    User getDetail(@PathVariable("id") Long id) {
        User user = userService.findById(id);
        if (user == null) throw new RuntimeException("not found");
        // do something with user
        return user;
    }
}

這段代碼所實(shí)現(xiàn)的是根據(jù)id獲取Entity對(duì)象,然后判斷Entity對(duì)象是否存在哟玷,如果不存在則直接拋出異常希停,避免接下來的操作。
可以看到淳衙,只要有根據(jù)id獲取Entity的地方蘑秽,就會(huì)出現(xiàn)上面這種模式的代碼,一個(gè)成熟的項(xiàng)目箫攀,這些模式少說也要出現(xiàn)十幾二十次肠牲,代碼重復(fù)多了,寫起來累靴跛,而且還容易出bug缀雳,比如有的地方?jīng)]有對(duì)Entity做非null校驗(yàn),就有可能出NPE了梢睛。

去除重復(fù)代碼肥印,提高健壯性

如果Spring能夠直接從id獲取Entity识椰,并且注入到getDetail的參數(shù)中,就可以避免這些重復(fù)的代碼深碱,就像這樣:

class UserController {
    @Autowired
    UserService userService;

    @GetMapping("/{id}")
    User getDetail(@PathVariable("id") User user) {
        // do something with user
        return user;
    }
}

并且在注入user的同時(shí)腹鹉,還能判斷是否user != null莹痢,如果成立直接拋出異常种蘸,并且返回404。

初步解決方案

Spring其實(shí)已經(jīng)提供了操作controller參數(shù)的方法竞膳,如果用的@PathVariable注解controller method的參數(shù)航瞭,Spring會(huì)調(diào)用PathVariableMethodArgumentResolver對(duì)url中的參數(shù)進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換結(jié)果就是controller method的參數(shù)坦辟。

PathVariableMethodArgumentResolver在轉(zhuǎn)換時(shí)刊侯,會(huì)先根據(jù)類型找到對(duì)應(yīng)的converter,然后調(diào)用Converter轉(zhuǎn)換锉走。所以可以增加一個(gè)自定義的Converter滨彻,把id轉(zhuǎn)化為user,如下:

@Component
public class IdToUserConverter implements Converter<String, User> {
    @Autowired
    UserMapper userMapper;

    @Override
    public User convert(String source) {
        User user = userMapper.selectByPrimaryKey(source);
        if (user == null) throw new RuntimeException("not found");
        return user;
    }
}

并且還要將自定義的IdToUserConverter注冊到Spring的converter庫里挪蹭。

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
   
    @Autowired
    IdToUserConverter idToUserConverter;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(idToUserConverter);
    }
}

這下只要在controller這樣寫亭饵,

User getDetail(@PathVariable("id") User user) {...}

就解決了寫重復(fù)代碼和校驗(yàn)user!=null的問題梁厉,避免寫重復(fù)代碼辜羊。

優(yōu)化解決方案

不過這樣寫還有個(gè)問題,現(xiàn)實(shí)情況下不可能只有User一個(gè)Entity词顾,如果每個(gè)Entity都要寫一個(gè)IdToSomeEntityConverter八秃,還是很麻煩。
要解決這個(gè)問題肉盹,需要一個(gè)前提條件昔驱,就是必須使用統(tǒng)一的dao層,并且必須給Entity一個(gè)統(tǒng)一的類型上忍。我使用的是tk mybatis為mapper提供統(tǒng)一的接口方法骤肛。代碼如下:

public interface UserMapper extends BaseMapper<User> {}

public interface RoleMapper extends BaseMapper<Role> {}

然后定義一個(gè)標(biāo)簽接口,所有Entity類都實(shí)現(xiàn)這個(gè)接口

interface SupportConverter {}

class User implement SupportConverter {...} 

class Role implement SupportConverter {...}

BaseMapper提供了selectByPrimaryKey方法窍蓝,可以根據(jù)Entity的Id獲取Entity萌衬。如果所有的mapper都有這個(gè)方法,那就方便進(jìn)行統(tǒng)一處理了它抱。

除了統(tǒng)一mapper的接口秕豫,原來的IdToUserConverter只能處理User一種類型,為了處理多種Entity類型,要把Converter換成ConverterFactory混移,ConverterFactory可以支持對(duì)一個(gè)類型的子類型選擇對(duì)應(yīng)的converter祠墅。ConverterFactory實(shí)現(xiàn)如下:

@Component
public class IdToEntityConverterFactory implements ConverterFactory<String, SupportConverter> {

    private static final Map<Class,Converter> CONVERTER_MAP=new HashMap<>();

    @Autowired
    ApplicationContext applicationContext;

    @Override
    public <T extends SupportConverter> Converter<String, T> getConverter(Class<T> targetType) {
        if (CONVERTER_MAP.get(targetType) == null) {
            CONVERTER_MAP.put(targetType, new IdToEntityConverter(targetType));
        }
        return CONVERTER_MAP.get(targetType);
    }

    private class IdToEntityConverter<T extends Audit> implements Converter<String, T> {
        private final Class<T> tClass;

        public IdToEntityConverter(Class<T> tClass) {
            this.tClass = tClass;
        }

        @Override
        public T convert(String source) {
            String[] beanNames = applicationContext.getBeanNamesForType(ResolvableType.forClassWithGenerics(BaseMapper.class, tClass));
            BaseMapper mapper = (BaseMapper) applicationContext.getBean(beanNames[0]);
            T result = (T) mapper.selectByPrimaryKey(Long.parseLong(source));
            if (result == null) throw new DataNotFoundException(tClass.getSimpleName() + " not found");
            return result;
        }
    }
}

最后,把自定義的IdToEntityConverterFactory注冊到Spring的formatter歌径,

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
   
    @Autowired
    IdToEntityConverterFactory idToEntityConverterFactory;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(idToEntityConverterFactory);
    }
}

現(xiàn)在代碼重復(fù)的問題解決了毁嗦,如果后續(xù)要增加新的Entity,只要讓Entity實(shí)現(xiàn)SupportConverter回铛,并且提供繼承BaseMapper的mapper狗准,那么就可以自動(dòng)支持@PathVariable 注解參數(shù)轉(zhuǎn)Entity對(duì)象了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茵肃,一起剝皮案震驚了整個(gè)濱河市腔长,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌验残,老刑警劉巖捞附,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異您没,居然都是意外死亡鸟召,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門氨鹏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欧募,“玉大人,你說我怎么就攤上這事仆抵「蹋” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵肢础,是天一觀的道長。 經(jīng)常有香客問我碌廓,道長传轰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任谷婆,我火速辦了婚禮慨蛙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纪挎。我一直安慰自己期贫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布异袄。 她就那樣靜靜地躺著通砍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上封孙,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天迹冤,我揣著相機(jī)與錄音,去河邊找鬼虎忌。 笑死泡徙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膜蠢。 我是一名探鬼主播堪藐,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挑围!你這毒婦竟也來了礁竞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤贪惹,失蹤者是張志新(化名)和其女友劉穎苏章,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奏瞬,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枫绅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了硼端。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片并淋。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖珍昨,靈堂內(nèi)的尸體忽然破棺而出县耽,到底是詐尸還是另有隱情,我是刑警寧澤镣典,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布兔毙,位于F島的核電站,受9級(jí)特大地震影響兄春,放射性物質(zhì)發(fā)生泄漏澎剥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一赶舆、第九天 我趴在偏房一處隱蔽的房頂上張望哑姚。 院中可真熱鬧,春花似錦芜茵、人聲如沸叙量。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绞佩。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間征炼,已是汗流浹背析既。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谆奥,地道東北人眼坏。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像酸些,于是被迫代替她去往敵國和親宰译。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理魄懂,服務(wù)發(fā)現(xiàn)沿侈,斷路器,智...
    卡卡羅2017閱讀 134,601評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,748評(píng)論 6 342
  • 要加“m”說明是MB市栗,否則就是KB了. -Xms:初始值 -Xmx:最大值 -Xmn:最小值 java -Xms8...
    dadong0505閱讀 4,806評(píng)論 0 53
  • 有時(shí)候會(huì)覺得自己仿佛被世界拋棄缀拭,孤獨(dú)感在自己內(nèi)心無限的擴(kuò)大,最終沉溺在黑暗的角落里填帽,聽著悲哀的歌曲蛛淋、看著漫無邊際的...
    KaisenH閱讀 245評(píng)論 4 3
  • 我凝望夜空中閃爍的群星 從沒有一顆向我眨眼 我諦聽寂靜中細(xì)碎的聲響 每一聲都讓我想起家鄉(xiāng) 我回憶童年時(shí)美好的畫面 ...
    東京森林閱讀 247評(píng)論 0 2