設(shè)計(jì)之道-controller層的設(shè)計(jì)

最近想把平時(shí)工作中總結(jié)出來(lái)的一些技巧和最佳實(shí)踐分享給大家,主要包含java編程和數(shù)據(jù)庫(kù)設(shè)計(jì),本篇著重于web應(yīng)用開(kāi)發(fā)中controller層的實(shí)踐。

在講controller層的設(shè)計(jì)之前,我想先簡(jiǎn)單講講web應(yīng)用的工程結(jié)構(gòu)。一般來(lái)說(shuō),我們的web工程結(jié)構(gòu)會(huì)分為三層,自下而上是dao層,service層controller層。

  • dao層是數(shù)據(jù)層,直接進(jìn)行數(shù)據(jù)庫(kù)的讀寫(xiě)操作,返回?cái)?shù)據(jù)對(duì)象DO,DO與數(shù)據(jù)庫(kù)表一一對(duì)應(yīng)。
  • service層為業(yè)務(wù)層,用來(lái)實(shí)現(xiàn)業(yè)務(wù)邏輯。能調(diào)用dao層或者service層,返回?cái)?shù)據(jù)對(duì)象DO或者業(yè)務(wù)對(duì)象BO,BO通常由DO轉(zhuǎn)化、整合而來(lái),可以包含多個(gè)DO的屬性,也可以是只包含一個(gè)DO的部分屬性。通常為了簡(jiǎn)便,如果無(wú)需轉(zhuǎn)化,service也可以直接返回DO。外部調(diào)用(HTTP、RPC)方法也在這一層,對(duì)于外部調(diào)用來(lái)說(shuō),service一般會(huì)將外部調(diào)用返回的DTO轉(zhuǎn)化為BO。
  • controller層為控制層,主要處理外部請(qǐng)求。調(diào)用service層,將service層返回的BO/DO轉(zhuǎn)化為DTO/VO并封裝成統(tǒng)一返回對(duì)象返回給調(diào)用方。如果返回?cái)?shù)據(jù)用于前端模版渲染則返回VO,否則一般返回DTO。不論是DTO還是VO,一般都會(huì)對(duì)BO/DO中的數(shù)據(jù)進(jìn)行一些轉(zhuǎn)化和整合,比如將gender屬性中的0轉(zhuǎn)化“男”,1轉(zhuǎn)化為“女”等。
代碼結(jié)構(gòu).png

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

我會(huì)拿一個(gè)簡(jiǎn)單的業(yè)務(wù)操作:變更用戶信息并且返回更新后的用戶信息作為例子,來(lái)介紹一下controller層的設(shè)計(jì)理念。

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

/**
 * @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ù)校驗(yàn),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)換對(duì)象,轉(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());
            }
            //組裝返回對(duì)象
            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;
}
}

是不是覺(jué)得很繁瑣?
沒(méi)錯(cuò),這里有許多值得優(yōu)化的地方,接下來(lái)我會(huì)帶大家一步一步優(yōu)化controller層的代碼,最終實(shí)現(xiàn)只需要寫(xiě)一行就能完成所有的工作。

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

首先,我們看到這里無(wú)論是業(yè)務(wù)成功或者失敗,都需要封裝返回對(duì)象,非常麻煩,我們應(yīng)該就能想到把封裝返回對(duì)象的邏輯抽象出來(lái),寫(xiě)到一個(gè)BaseController類中,供所有的controller繼承:

/**
 * @Author: Sawyer
 * @Description: 基礎(chǔ)controller,用來(lái)包裝http返回對(duì)象
 * @Date: Created in 上午10:43 17/8/11
 */
public abstract class BaseController {

/**
 * 默認(rèn)成功返回
 *
 * @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;
}

/**
 * 默認(rèn)成功返回帶消息
 *
 * @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;
}

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

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

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

/**
 * 失敗返回 
 *
 * @param code    錯(cuò)誤Code
 * @param message 若為null,則使用Code對(duì)應(yīng)的默認(rèn)信息
 * @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;
}
}

這個(gè)BaseController主要提供了封裝返回對(duì)象的幾種方法,可以滿足成功或者失敗的返回情況。將我們的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ù)校驗(yàn),UserDTO.name不能為空
    if (userDTO.getName() == null) {
        return responseFail("name不能為空");
    } else {
        //調(diào)用service更新user,更新可能拋出異常,要捕獲
        try {
            User updatedUser = userService.updateUser(id, userDTO);
            //轉(zhuǎn)換對(duì)象,轉(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.對(duì)象轉(zhuǎn)化方法抽象

這里我們將User轉(zhuǎn)化為了UserDTO對(duì)象,并且根據(jù)User中的gender屬性設(shè)置了dto中相應(yīng)genderDesc的值,我們也很容易想到這個(gè)轉(zhuǎn)化方法應(yīng)該具有通用性,所以可以直接放到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的代碼可改寫(xiě)為:

@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ù)校驗(yàn),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ù)校驗(yàn)在對(duì)象中做

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

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

并且在形參上加上@Valid注解,這樣javax.validation將會(huì)幫我們校驗(yàn)參數(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();
    }
}
}

除了簡(jiǎn)單的非空判斷以外,我們也可以通過(guò)自定義注解來(lái)實(shí)現(xiàn)更復(fù)雜的邏輯判斷,這里就不展開(kāi)了,感興趣的同學(xué)可以自行百度。
有的朋友要問(wèn)了,不對(duì)啊,你這要是沒(méi)通過(guò)驗(yàn)證會(huì)返回500服務(wù)器錯(cuò)誤的啊,別急,我們來(lái)看最后一步。

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

如果sevice層的代碼都會(huì)拋出異常,難道我們需要在每個(gè)controller層的方法中都做try-catch嗎?顯然不是,我們可以給controller層的方法加上切面來(lái)統(tǒng)一處理異常。spring給我們提供了@ControllerAdvice注解,用來(lái)定義controller層的切面,所有@Controller注解的類中的方法執(zhí)行都會(huì)進(jìn)入該切面,同時(shí)我們可以使用@ExceptionHandler來(lái)對(duì)不同的異常進(jìn)行捕獲和處理,對(duì)于捕獲的異常,我們應(yīng)該進(jìn)行日志記錄,并且封裝返回對(duì)象:

/**
 * @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("報(bào)錯(cuò)API URL: %s%nQuery String: %s",
            httpServletRequest.getRequestURI(),
            httpServletRequest.getQueryString());
    ApiLogger.runLogger.error(info);
    ApiLogger.exceptionLogger.error(e.getMessage(), e);
    String ipInfo = "報(bào)錯(cuò)訪問(wèn)者IP信息:" + httpServletRequest.getRemoteAddr() + "," + httpServletRequest.getRemoteHost();
    ApiLogger.runLogger.error(ipInfo);
}

/**
 * 參數(shù)校驗(yàn)異常
 *
 * @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);
}

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

/**
 * 業(yè)務(wù)層異常
 *
 * @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);
}
}

上面這個(gè)切面是我日常工作在用的切面,基本涵蓋了常見(jiàn)的web異常,在這里也可以通過(guò)@ExceptionHandler處理自定義的異常,比如這里的serviceException?;卮鹕厦孢z留下來(lái)的問(wèn)題,如果沒(méi)有通過(guò)參數(shù)校驗(yàn),那么就會(huì)被methodArgumentNotValid方法處理,并且妥善地使用baseController中的方法封裝好返回對(duì)象。加上切面后,我們的UserController代碼最終改寫(xiě)為:

@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)));
}
}

怎么樣?是不是變得特別簡(jiǎn)潔。通過(guò)統(tǒng)一的返回對(duì)象封裝,統(tǒng)一的異常處理等,我們的controller層代碼變得非常簡(jiǎn)介,這也是我個(gè)人比較推崇的代碼簡(jiǎn)約之道。學(xué)會(huì)的老鐵扣波666。

最后我想提的一點(diǎn)是,有些同學(xué)習(xí)慣在service層直接將HTTP/RPC返回對(duì)象封裝好返回,我個(gè)人認(rèn)為是不妥的。主要原因是如果在service就返回HTTP reponse,那么service層的互相調(diào)用都會(huì)面臨先判斷response的成功與否,再將reponse的data取出進(jìn)行邏輯操作,十分不便。畢竟現(xiàn)在沒(méi)有別的sevice調(diào)用,并不代表將來(lái)不會(huì)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 昨天花了半個(gè)小時(shí)的時(shí)間讀了這本書(shū),卻花了好幾個(gè)小時(shí)的時(shí)間來(lái)沉淀和思考。 說(shuō)實(shí)話,一開(kāi)始我覺(jué)得自己無(wú)法領(lǐng)悟,如此簡(jiǎn)單...
    傳世玉印閱讀 226評(píng)論 0 1
  • 1986年4月22日-2015年4月22日。我在這個(gè)世界上已經(jīng)完整的度過(guò)了28個(gè)年頭。今天是我29歲的第一天。 今...
    OScarsab閱讀 718評(píng)論 2 3
  • 前些日子,我身體不舒服去醫(yī)院檢查。 因?yàn)閾?dān)心醫(yī)生要午休,我特地等到了二點(diǎn)以后才去的,誰(shuí)知道到了醫(yī)院里,婦產(chǎn)科的辦公...
    藍(lán)雅飄奕閱讀 338評(píng)論 0 0
  • 暑假期間,伴隨著兒子升學(xué),老父親制造了兩個(gè)有趣的故事,說(shuō)給大家聽(tīng)聽(tīng)。 第一個(gè)是花生米的故事。前幾天...
    松峰說(shuō)教劉樹(shù)森閱讀 644評(píng)論 0 8

友情鏈接更多精彩內(nèi)容