設計之道-controller層的設計

最近想把平時工作中總結出來的一些技巧和最佳實踐分享給大家量愧,主要包含java編程和數(shù)據(jù)庫設計灯帮,本篇著重于web應用開發(fā)中controller層的實踐肪虎。

在講controller層的設計之前做瞪,我想先簡單講講web應用的工程結構契讲。一般來說畸裳,我們的web工程結構會分為三層缰犁,自下而上是dao層service層controller層怖糊。

  • dao層是數(shù)據(jù)層帅容,直接進行數(shù)據(jù)庫的讀寫操作,返回數(shù)據(jù)對象DO伍伤,DO與數(shù)據(jù)庫表一一對應并徘。
  • service層為業(yè)務層,用來實現(xiàn)業(yè)務邏輯扰魂。能調(diào)用dao層或者service層麦乞,返回數(shù)據(jù)對象DO或者業(yè)務對象BO蕴茴,BO通常由DO轉(zhuǎn)化、整合而來路幸,可以包含多個DO的屬性荐开,也可以是只包含一個DO的部分屬性。通常為了簡便简肴,如果無需轉(zhuǎn)化晃听,service也可以直接返回DO。外部調(diào)用(HTTP砰识、RPC)方法也在這一層能扒,對于外部調(diào)用來說,service一般會將外部調(diào)用返回的DTO轉(zhuǎn)化為BO辫狼。
  • controller層為控制層初斑,主要處理外部請求。調(diào)用service層膨处,將service層返回的BO/DO轉(zhuǎn)化為DTO/VO并封裝成統(tǒng)一返回對象返回給調(diào)用方见秤。如果返回數(shù)據(jù)用于前端模版渲染則返回VO,否則一般返回DTO真椿。不論是DTO還是VO鹃答,一般都會對BO/DO中的數(shù)據(jù)進行一些轉(zhuǎn)化和整合,比如將gender屬性中的0轉(zhuǎn)化“男”突硝,1轉(zhuǎn)化為“女”等测摔。
代碼結構.png

了解了工程結構后,我們可以來講講controller層的設計解恰。首先明確一點锋八,除了極少數(shù)不復用的簡單處理,controller層不應該包含業(yè)務邏輯护盈,controller的功能應該有以下五點:
1.參數(shù)校驗
2.調(diào)用service層接口實現(xiàn)業(yè)務邏輯
3.轉(zhuǎn)換業(yè)務/數(shù)據(jù)對象
4.組裝返回對象
5.異常處理

我會拿一個簡單的業(yè)務操作:變更用戶信息并且返回更新后的用戶信息作為例子挟纱,來介紹一下controller層的設計理念。

下面是一個最普通的寫法腐宋,這里已經(jīng)將更新并返回用戶信息的業(yè)務邏輯全放在了service層樊销,但是controller層仍需要承擔參數(shù)校驗、轉(zhuǎn)換對象脏款、組裝返回對象和異常處理的工作:

/**
 * @Author: Sawyer
 * @Description:
 * @Date: Created in 下午5:43 18/9/1
 */
@RestController
@RequestMapping("/v1/user")
public class UserController {

@Autowired
UserService userService;

@PutMapping("/{id}")
public HttpResult<UserDTO> updateUser(@PathVariable("id") Integer id, @RequestBody UserDTO userDTO) {
    HttpResult<UserDTO> result = new HttpResult<>();
    //參數(shù)校驗,UserDTO.name不能為空
    if (userDTO.getName() == null) {
        result.setSuccess(false);
        result.setCode(ResultCode.INVALID_PARAM.getCode());
        result.setMessage("name不能為空");
    } else {
        //調(diào)用service更新user裤园,更新可能拋出異常撤师,要捕獲
        try {
            User updatedUser = userService.updateUser(id, userDTO);
            //轉(zhuǎn)換對象,轉(zhuǎn)化DO為DTO
            UserDTO updatedDto = new UserDTO();
            BeanUtils.copyProperties(updatedUser, updatedDto);
            if (GenderEnum.MALE.getCode() == updatedUser.getGender()) {
                updatedDto.setGenderDesc(GenderEnum.MALE.name());
            } else {
                updatedDto.setGenderDesc(GenderEnum.FEMALE.name());
            }
            //組裝返回對象
            result.setData(updatedDto);
            result.setSuccess(true);
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(ResultCode.SUCCESS.getMessage());
        } catch (ServiceEx ex) {
            //異常處理
            result.setSuccess(false);
            result.setCode(ResultCode.SYSTEM_ERROR.getCode());
            result.setMessage(ex.getMessage());
        }

    }
    return result;
}
}

是不是覺得很繁瑣拧揽?
沒錯剃盾,這里有許多值得優(yōu)化的地方腺占,接下來我會帶大家一步一步優(yōu)化controller層的代碼,最終實現(xiàn)只需要寫一行就能完成所有的工作痒谴。

1.統(tǒng)一封裝返回對象

首先衰伯,我們看到這里無論是業(yè)務成功或者失敗,都需要封裝返回對象积蔚,非常麻煩意鲸,我們應該就能想到把封裝返回對象的邏輯抽象出來,寫到一個BaseController類中尽爆,供所有的controller繼承:

/**
 * @Author: Sawyer
 * @Description: 基礎controller怎顾,用來包裝http返回對象
 * @Date: Created in 上午10:43 17/8/11
 */
public abstract class BaseController {

/**
 * 默認成功返回
 *
 * @param data
 * @return
 */
protected <T> HttpResult<T> responseOK(T data) {
    HttpResult<T> restResult = new HttpResult<>();
    restResult.setSuccess(true);
    restResult.setData(data);
    restResult.setCode(ResultCode.SUCCESS.getCode());
    restResult.setMessage(ResultCode.SUCCESS.getMessage());
    return restResult;
}

/**
 * 默認成功返回帶消息
 *
 * @param data
 * @param msg
 * @return
 */
protected <T> HttpResult<T> responseOK(T data, String msg) {
    HttpResult<T> restResult = new HttpResult<>();
    restResult.setSuccess(true);
    restResult.setData(data);
    restResult.setCode(ResultCode.SUCCESS.getCode());
    restResult.setMessage(msg);
    return restResult;
}

/**
 * 默認失敗返回, 不帶參數(shù)
 *
 * @return
 */
protected <T> HttpResult<T> responseFail() {
    return responseFail(ResultCode.SYSTEM_ERROR);
}

/**
 * 默認失敗返回, 帶信息
 *
 * @param message 
 * @return
 */
protected <T> HttpResult<T> responseFail(String message) {
    return responseFail(ResultCode.SYSTEM_ERROR, message);
}

/**
 * 默認失敗返回,帶code
 *
 * @param code
 * @return
 */
protected <T> HttpResult<T> responseFail(ResultCode code) {
    return responseFail(code, code.getMessage());
}

/**
 * 失敗返回 
 *
 * @param code    錯誤Code
 * @param message 若為null漱贱,則使用Code對應的默認信息
 * @return
 */
protected <T> HttpResult<T> responseFail(ResultCode code, String message) {
    HttpResult<T> restResult = new HttpResult<>();
    restResult.setSuccess(false);
    restResult.setCode(code.getCode());
    message = message == null ? code.getMessage() : message;
    restResult.setMessage(message);
    return restResult;
}
}

這個BaseController主要提供了封裝返回對象的幾種方法槐雾,可以滿足成功或者失敗的返回情況。將我們的UserController繼承該類幅狮,使用這里的方法后代碼變?yōu)椋?/p>

@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController {

@Autowired
UserService userService;

@PutMapping("/{id}")
public HttpResult<UserDTO> updateUser(@PathVariable("id") Integer id, @RequestBody UserDTO userDTO) {
    //參數(shù)校驗募强,UserDTO.name不能為空
    if (userDTO.getName() == null) {
        return responseFail("name不能為空");
    } else {
        //調(diào)用service更新user,更新可能拋出異常崇摄,要捕獲
        try {
            User updatedUser = userService.updateUser(id, userDTO);
            //轉(zhuǎn)換對象擎值,轉(zhuǎn)化DO為DTO
            UserDTO updatedDto = new UserDTO();
            BeanUtils.copyProperties(updatedUser, updatedDto);
            if (GenderEnum.MALE.getCode() == updatedUser.getGender()) {
                updatedDto.setGenderDesc(GenderEnum.MALE.name());
            } else {
                updatedDto.setGenderDesc(GenderEnum.FEMALE.name());
            }
            return responseOK(updatedDto);
        } catch (ServiceEx ex) {
            //異常處理
            return responseFail();
        }
    }
}
}

2.對象轉(zhuǎn)化方法抽象

這里我們將User轉(zhuǎn)化為了UserDTO對象,并且根據(jù)User中的gender屬性設置了dto中相應genderDesc的值配猫,我們也很容易想到這個轉(zhuǎn)化方法應該具有通用性幅恋,所以可以直接放到UserDTO中:

public class UserDTO {

private Integer id;

private String name;

private String gender;

private String genderDesc;

private Date createdTime;

public static UserDTO convert(User user) {
    Assert.notNull(user, "user不能為空");
    UserDTO dto = new UserDTO();
    BeanUtils.copyProperties(user, dto);
    if (GenderEnum.MALE.getCode() == user.getGender()) {
        dto.setGenderDesc(GenderEnum.MALE.name());
    } else {
        dto.setGenderDesc(GenderEnum.FEMALE.name());
    }
    return dto;
}
//getter and setter
}

那么我們的UserController的代碼可改寫為:

@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController {

@Autowired
UserService userService;

@PutMapping("/{id}")
public HttpResult<UserDTO> updateUser(@PathVariable("id") Integer id, @RequestBody UserDTO userDTO) {
    //參數(shù)校驗,UserDTO.name不能為空
    if (userDTO.getName() == null) {
        return responseFail("name不能為空");
    } else {
        //調(diào)用service更新user泵肄,更新可能拋出異常捆交,要捕獲
        try {
            return responseOK(UserDTO.convert(userService.updateUser(id, userDTO)));
        } catch (ServiceEx ex) {
            //異常處理
            return responseFail();
        }
    }
}
}

3.參數(shù)校驗在對象中做

其實大多數(shù)的參數(shù)校驗無非就是判空或者空字符串,那么我們可以好好利用javax.validation為我們提供的@NotNull等注解腐巢。在UserDTO類中name屬性上加上@NotNull字段:

@NotNull(message = "name不能為空")
private String name;

并且在形參上加上@Valid注解品追,這樣javax.validation將會幫我們校驗參數(shù):

@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController {

@Autowired
UserService userService;

@PutMapping("/{id}")
public HttpResult<UserDTO> updateUser(@PathVariable("id") Integer id, @Valid @RequestBody UserDTO userDTO) {
    //調(diào)用service更新user,更新可能拋出異常冯丙,要捕獲
    try {
        return responseOK(UserDTO.convert(userService.updateUser(id, userDTO)));
    } catch (ServiceEx ex) {
        //異常處理
        return responseFail();
    }
}
}

除了簡單的非空判斷以外肉瓦,我們也可以通過自定義注解來實現(xiàn)更復雜的邏輯判斷,這里就不展開了胃惜,感興趣的同學可以自行百度泞莉。
有的朋友要問了,不對啊船殉,你這要是沒通過驗證會返回500服務器錯誤的啊鲫趁,別急,我們來看最后一步利虫。

4.統(tǒng)一的異常捕獲

如果sevice層的代碼都會拋出異常挨厚,難道我們需要在每個controller層的方法中都做try-catch嗎堡僻?顯然不是,我們可以給controller層的方法加上切面來統(tǒng)一處理異常疫剃。spring給我們提供了@ControllerAdvice注解钉疫,用來定義controller層的切面,所有@Controller注解的類中的方法執(zhí)行都會進入該切面巢价,同時我們可以使用@ExceptionHandler來對不同的異常進行捕獲和處理牲阁,對于捕獲的異常,我們應該進行日志記錄蹄溉,并且封裝返回對象:

/**
 * @Author: Sawyer
 * @Description: 統(tǒng)一異常處理
 * @Date: Created in 上午11:17 17/8/11
 */
@ControllerAdvice
@RestController
public class ExceptionAdvice extends BaseController {

@Autowired
HttpServletRequest httpServletRequest;

/**
 * 
 * 異常日志記錄
 *
 * @param e
 */
private void logErrorRequest(Exception e) {
    String info = String.format("報錯API URL: %s%nQuery String: %s",
            httpServletRequest.getRequestURI(),
            httpServletRequest.getQueryString());
    ApiLogger.runLogger.error(info);
    ApiLogger.exceptionLogger.error(e.getMessage(), e);
    String ipInfo = "報錯訪問者IP信息:" + httpServletRequest.getRemoteAddr() + "," + httpServletRequest.getRemoteHost();
    ApiLogger.runLogger.error(ipInfo);
}

/**
 * 參數(shù)校驗異常
 *
 * @param exception
 * @return
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
protected HttpResult methodArgumentNotValid(MethodArgumentNotValidException exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.INVALID_PARAM);
}

/**
 * 參數(shù)格式有誤
 *
 * @param exception
 * @return
 */
@ExceptionHandler({MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class})
protected HttpResult typeMismatch(Exception exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.MISTYPE_PARAM);
}

/**
 * 缺少參數(shù)
 *
 * @param exception
 * @return
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
protected HttpResult missingServletRequestParameter(MissingServletRequestParameterException exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.MISSING_PARAM);
}

/**
 * 不支持的請求類型
 *
 * @param exception
 * @return
 */
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected HttpResult httpRequestMethodNotSupported(HttpRequestMethodNotSupportedException exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.UNSUPPORTED_METHOD);
}

/**
 * 業(yè)務層異常
 *
 * @param exception
 * @return
 */
@ExceptionHandler(ServiceEx.class)
protected HttpResult serviceException(ServiceEx exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.SYSTEM_ERROR, exception.getMessage());
}

/**
 * 其他異常
 *
 * @param exception
 * @return
 */
@ExceptionHandler({HttpClientErrorException.class, IOException.class, Exception.class})
protected HttpResult commonException(Exception exception) {
    logErrorRequest(exception);
    return responseFail(ResultCode.SYSTEM_ERROR);
}
}

上面這個切面是我日常工作在用的切面咨油,基本涵蓋了常見的web異常,在這里也可以通過@ExceptionHandler處理自定義的異常柒爵,比如這里的serviceException役电。回答上面遺留下來的問題棉胀,如果沒有通過參數(shù)校驗法瑟,那么就會被methodArgumentNotValid方法處理,并且妥善地使用baseController中的方法封裝好返回對象唁奢。加上切面后霎挟,我們的UserController代碼最終改寫為:

@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController {

@Autowired
UserService userService;

@PutMapping("/{id}")
public HttpResult<UserDTO> updateUser(@PathVariable("id") Integer id, @Valid @RequestBody UserDTO userDTO) throws Exception { 
    return responseOK(UserDTO.convert(userService.updateUser(id, userDTO)));
}
}

怎么樣?是不是變得特別簡潔麻掸。通過統(tǒng)一的返回對象封裝酥夭,統(tǒng)一的異常處理等,我們的controller層代碼變得非常簡介脊奋,這也是我個人比較推崇的代碼簡約之道熬北。學會的老鐵扣波666。

最后我想提的一點是诚隙,有些同學習慣在service層直接將HTTP/RPC返回對象封裝好返回讶隐,我個人認為是不妥的。主要原因是如果在service就返回HTTP reponse久又,那么service層的互相調(diào)用都會面臨先判斷response的成功與否巫延,再將reponse的data取出進行邏輯操作,十分不便地消。畢竟現(xiàn)在沒有別的sevice調(diào)用炉峰,并不代表將來不會。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脉执,一起剝皮案震驚了整個濱河市疼阔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌适瓦,老刑警劉巖竿开,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異玻熙,居然都是意外死亡否彩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門嗦随,熙熙樓的掌柜王于貴愁眉苦臉地迎上來列荔,“玉大人,你說我怎么就攤上這事枚尼√悖” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵署恍,是天一觀的道長崎溃。 經(jīng)常有香客問我,道長盯质,這世上最難降的妖魔是什么袁串? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮呼巷,結果婚禮上囱修,老公的妹妹穿的比我還像新娘。我一直安慰自己王悍,他們只是感情好破镰,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著压储,像睡著了一般鲜漩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渠脉,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天宇整,我揣著相機與錄音,去河邊找鬼芋膘。 笑死鳞青,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的为朋。 我是一名探鬼主播臂拓,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼习寸!你這毒婦竟也來了胶惰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤霞溪,失蹤者是張志新(化名)和其女友劉穎孵滞,沒想到半個月后中捆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坊饶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年泄伪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匿级。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡蟋滴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出痘绎,到底是詐尸還是另有隱情津函,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布孤页,位于F島的核電站尔苦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏散庶。R本人自食惡果不足惜蕉堰,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悲龟。 院中可真熱鬧屋讶,春花似錦、人聲如沸须教。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻腺。三九已至乐疆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贬养,已是汗流浹背挤土。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留误算,地道東北人仰美。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像儿礼,于是被迫代替她去往敵國和親咖杂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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

  • 昨天花了半個小時的時間讀了這本書蚊夫,卻花了好幾個小時的時間來沉淀和思考诉字。 說實話,一開始我覺得自己無法領悟,如此簡單...
    傳世玉印閱讀 188評論 0 1
  • 1986年4月22日-2015年4月22日壤圃。我在這個世界上已經(jīng)完整的度過了28個年頭陵霉。今天是我29歲的第一天。 今...
    OScarsab閱讀 646評論 2 3
  • 前些日子伍绳,我身體不舒服去醫(yī)院檢查撩匕。 因為擔心醫(yī)生要午休,我特地等到了二點以后才去的墨叛,誰知道到了醫(yī)院里,婦產(chǎn)科的辦公...
    藍雅飄奕閱讀 283評論 0 0
  • 暑假期間模蜡,伴隨著兒子升學漠趁,老父親制造了兩個有趣的故事,說給大家聽聽忍疾。 第一個是花生米的故事闯传。前幾天...
    松峰說教劉樹森閱讀 598評論 0 8