最近想把平時工作中總結出來的一些技巧和最佳實踐分享給大家量愧,主要包含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)化為“女”等测摔。
了解了工程結構后,我們可以來講講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)用炉峰,并不代表將來不會。