最近想把平時(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)后,我們可以來(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ì)。